【cplusplus教程翻译】指针(Pointers)

发布时间 2023-05-27 15:52:01作者: xiaoweing

在前面的章节中,变量被解释为计算机内存中的位置,可以通过其标识符(名称)访问这些位置。这样,程序就不需要关心内存中数据的物理地址;只要需要引用变量,它就简单地使用标识符。
对于C++程序来说,计算机的内存就像一系列的存储单元,每个存储单元的大小都是一个字节,并且每个存储单元都有一个唯一的地址。这些单字节存储单元的排序方式允许大于一个字节的数据表示占据具有连续地址的存储单元。
这样,每个单元都可以通过其唯一地址轻松地定位在存储器中。例如,具有地址1776的存储器单元总是紧跟在具有地址1775的单元之后并且在具有1777的单元之前,并且正好是776之后的一千个单元并且正好是2776之前的一千个单元。
当一个变量被声明时,存储其值所需的内存被分配到内存中的一个特定位置(其内存地址)。通常,C++程序不会主动决定存储其变量的确切内存地址。幸运的是,这项任务留给了程序运行的环境——通常是一个在运行时决定特定内存位置的操作系统。然而,对于程序来说,能够在运行时获得变量的地址以访问相对于它处于特定位置的数据单元可能是有用的。

取地址符

变量的地址可以通过在变量的名称前面加一个与号(&)来获得,即运算符的地址。例如:foo = &myvar;
这将把变量myvar的地址分配给foo;通过在变量myvar的名称前面加上operator(&)的地址,我们不再将变量本身的内容分配给foo,而是将其地址分配给它。
内存中变量的实际地址在运行时之前是未知的,但为了帮助澄清一些概念,我们假设myvar在运行时被放置在内存地址1776中。考虑下面的代码

myvar = 25;
foo = &myvar;
bar = myvar;

内存模型如下

首先,我们把25赋值给myvar变量,这个变量的地址是1776
第二个语句把myvar的地址赋值给foo,最后一个语句把myvar变量赋值给bar
第二三个语句的区别就是取地址符
存变量地址的变量被称为指针,指针威力非常大,可以用于底层编程,下面介绍怎么声明和使用指针

解引用操作符

指针可以认为是图里的边,考虑指令集里的访存指令,这个操作被叫做解引用baz = *foo;

请注意解引用的区别

baz = foo;   // baz equal to foo (1776)
baz = *foo;  // baz equal to value pointed to by foo (25) 

把&和*认为是操作符进行思考,赋值也是操作符

myvar = 25;
foo = &myvar;

声明指针

由于指针能够直接引用它所指向的值,因此指针指向char时与指向int或float时具有不同的属性。一旦想要解引用,就需要知道类型。为此,指针的声明需要包括指针要指向的数据类型。
声明语法type * name,type是指向数据的类型,注意指针类型是单独的一种类型,和指向数据的类型不同

int * number;
char * character;
double * decimals;

上面这三个变量指向的类型不同,但是这三个变量的类型是相同的(都是地址,32位或64位),指针变量的大小是相同,但是指向的类型大小可能不同,能支持的操作也不同。注意这个*和解引用的不同

// my first pointer
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue, secondvalue;
  int * mypointer;

  mypointer = &firstvalue;
  *mypointer = 10;
  mypointer = &secondvalue;
  *mypointer = 20;
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}

指针变量可以随便改变赋值

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue = 5, secondvalue = 15;
  int * p1, * p2;

  p1 = &firstvalue;  // p1 = address of firstvalue
  p2 = &secondvalue; // p2 = address of secondvalue
  *p1 = 10;          // value pointed to by p1 = 10
  *p2 = *p1;         // value pointed to by p2 = value pointed to by p1
  p1 = p2;           // p1 = p2 (value of pointer is copied)
  *p1 = 20;          // value pointed to by p1 = 20
  
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}

注意声明int * p1, * p2;int * p1, p2;声明的变量不同,前面是两个指针,后面是一个指针一个整型变量

指针和数组

数组的概念与指针的概念有关。事实上,数组的工作方式非常类似于指向其第一个元素的指针,而且,实际上,数组总是可以隐式转换为正确类型的指针。例如,考虑以下两个声明:

int myarray [20];
int * mypointer;

然后可以赋值mypointer = myarray;,请注意指针是可以随便赋值的,数组可以认为是一个常量,编译器就确定了,恒指向20个整型元素

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int numbers[5];
  int * p;
  p = numbers;  *p = 10;
  p++;  *p = 20;
  p = &numbers[2];  *p = 30;
  p = numbers + 3;  *p = 40;
  p = numbers;  *(p+4) = 50;
  for (int n=0; n<5; n++)
    cout << numbers[n] << ", ";
  return 0;
}

指针和数组的操作也是相同的,考虑常量
请注意[] offset操作符,等价于解引用和指针加减法

a[5] = 0;       // a [offset of 5] = 0
*(a+5) = 0;     // pointed to by (a+5) = 0 

指针初始化

声明的时候也可以初始化,这是一个好习惯

int myvar;
int * myptr;
*myptr = &myvar; // 这句话不合法,其实这个两边的类型不同

第二行不是解引用操作符,空格并不影响语法

int myvar;
int *foo = &myvar;
int *bar = foo;

指针运算

指针只支持加减法,偏移量和指向的数据类型有关
char永远是1字节,short int long这些和系统有关,最好写成int32 int16这种

char *mychar;
short *myshort;
long *mylong;

加一的偏移量是不同的,注意优先级顺序

*p++   // same as *(p++): increment pointer, and dereference unincremented address
*++p   // same as *(++p): increment pointer, and dereference incremented address
++*p   // same as ++(*p): dereference pointer, and increment the value it points to
(*p)++ // dereference pointer, and post-increment the value it points to 

注意可读性*p++ = *q++;等价于```c++
*p = *q;
++p;
++q;

# 指针和常量
```c++
int x;
int y = 10;
const int * p = &y;  //可以非常转成常
x = *p;          // ok: reading p
*p = x;          // error: modifying p, which is const-qualified 

常量指针用于函数参数,可以和常引用类似

// pointers as arguments:
#include <iostream>
using namespace std;

void increment_all (int* start, int* stop)
{
  int * current = start;
  while (current != stop) {
    ++(*current);  // increment value pointed
    ++current;     // increment pointer
  }
}

void print_all (const int* start, const int* stop)
{
  const int * current = start;
  while (current != stop) {
    cout << *current << '\n';
    ++current;     // increment pointer
  }
}

int main ()
{
  int numbers[] = {10,20,30};
  increment_all (numbers,numbers+3);
  print_all (numbers,numbers+3);
  return 0;
}

常量指针只是不能改指向的值,但是指针本身可以自增

int x;
      int *       p1 = &x;  // non-const pointer to non-const int
const int *       p2 = &x;  // non-const pointer to const int
      int * const p3 = &x;  // const pointer to non-const int
const int * const p4 = &x;  // const pointer to const int 

const和指针的语法肯定很棘手,识别最适合每种使用的情况往往需要一些经验。无论如何,尽早获得指针(和引用)的常量是很重要的,但如果这是你第一次接触常量和指针的混合,你不应该太担心掌握一切。更多的用例将出现在接下来的章节中。
常量指针和指针常量的声明区别关键是const和*的顺序,类型的顺序无关

const int * p2a = &x;  //      non-const pointer to const int
int const * p2b = &x;  // also non-const pointer to const int

指针和字符串字面量

字符串字面量本质上是字符数组,可以用字符数组初始化,也可以用指针初始化,注意是否位常量const char * foo = "hello";,可以认为是无名变量,```c++
*(foo+4)
foo[4]

这样用是没问题的,都是字符o
# 指针的指针
多级指针用法,一定要注意复杂度
```c++
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;

链式法则

注意指针运算

指针空类型的指针

void类型的指针是一种特殊类型,void表示不定长度、不定解引用属性
void类型有很大的灵活性,可以进行类型转换,指向各种类型,不过要想解引用,还是要额外信息进行强转,最好使用模板

// increaser
#include <iostream>
using namespace std;

void increase (void* data, int psize)
{
  if ( psize == sizeof(char) )
  { char* pchar; pchar=(char*)data; ++(*pchar); }
  else if (psize == sizeof(int) )
  { int* pint; pint=(int*)data; ++(*pint); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  cout << a << ", " << b << '\n';
  return 0;
}

sizeof也是一个操作符,编译器就可以确定

无效指针和空指针

指针意味着地址,所有有有效无效之分

int * p;               // uninitialized pointer (local variable)

int myarray[10];
int * q = myarray+20;  // element out of bounds 

赋值的时候可以随便赋值,只有解引用的时候才能判断地址是否合法
空指针是一种特殊类型,可以有两种方式

int * p = 0;
int * q = nullptr;

NULL也是类似操作,只不过NULL不是关键字,也不是显式0,和头文件定义有关
注意空指针和void指针的区别,空指针是不指向任何地方,void指针是指向地方的类型不定

函数指针

函数指针可以用来作为另一个函数的参数(避免过多的if else,模板特化),函数指针的声明语法和正常函数声明一样,只不过把函数名用括号及*包括起来就行,注意和typedef等一起使用,类成员函数指针只能由类对象调用,注意声明语法

// pointer to functions
#include <iostream>
using namespace std;

int addition (int a, int b)
{ return (a+b); }

int subtraction (int a, int b)
{ return (a-b); }

int operation (int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*minus)(int,int) = subtraction;

  m = operation (7, 5, addition);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}

直接用函数名就可以初始化

总结

指针其实是底层存储模型的抽象,把各种内存都抽象成连续的字节序列,注意虚拟内存和物理内存的区别。