指针即地址。
1. 指针与变量。
2. 指针与数组。
3. 指针与字符串。
4. 指针与函数:指针函数(返回值为指针的函数)与函数指针(指向函数的指针)。
5. 指针数组与指向指针的指针。
6. 关于二维数组的指针
当我们定义int a[3][4] ;int (*p)[4]; 时,这时 p 与 a 是等价的指针变量。
假设 a 是二维数组名,则
a[0] 与 *(a+0) 等价;
a[i] + j 与 *(a+i)+j 等价;
二维数组元素 a[i][j] 可以表示为:
*(a[i]+j) 或 *(*(a+i)+j) 或 (*(a+i))[j] 。
7. const 指针
1)指向常量的指针变量(常值变量指针):
定义这种指针变量的一般形式为
const 类型名 * 指针变量名 或 类型名 const * 指针变量名。
如:
int a = 12, b = 15;
const int* p = &a; 或者 int const* p = &a;
不允许通过指针变量改变它所指向的对象的值,如:*p = 15;
但是指针变量 p 的值(即 p 的指向)是可以改变的,如:p = &b;
指针 p 本身并不用初始化。
注意: 用指向常量的指针变量只是限制了通过指针变量改变它所指向的对象的值,可以不通过 p 直接对 a 再赋值,如:a = 15;
如果想保证 a 的值始终不变,应当把 a 定义为常变量:const int a = 12;
这样 p 就成了指向常变量的指针变量。无论通过直接访问方式还是间接访问方式都无法改变 a 的值。
指向常量的指针常用于做函数形参,以防止指针形参所指向对象的值改变影响实参。
2)常指针:
指定指针变量的值是常量,即指针变量的指向不能改变,如:char* const p1 = "Canada";
定义这种指针变量的一般形式为:
类型名 * const 指针变量名;
必须在定义时初始化,指定其指向。
指针变量的指向不能改变,但指针变量指向的变量的值可以改变。
如:p1 = "America"; // 不合法
*p1 = 'B'; //合法
即可以通过指针变量对它指向的变量修改,更可以直接修改变量对其进行再赋值。
3)指向常量的常指针:
即:const 类型名 * const 指针变量名;
指针的指向本身不能改变,也不能通过指针变量改变该对象的值。
但仍然可以直接修改变量的值。
除非将变量定义为常变量。
4)注意常值变量 = 常变量以及他们与常量的区别。
5)注意如果要定义一个指向常变量的指针,只能用指向常量的指针变量,不能用普通指针或者常指针。当然第三种指针也可以。
8. void指针类型,不指向任何一个类型。
可以把非void型的指针赋给void型指针变量,但不能把void指针直接赋给非void型指针变量,必须进行强制类型转换。
9. 指针的运算。
10. 二维数组 int a[3][4]与 int (*p)[4](数组指针)、二级指针 int** b与 int* p[4](指针数组)
1)这两组实际上是对应关系:即可以令 a = p; b = q; 关于它们之间的区别我已经深刻理解,但是用语言表达出来却不太容易。
2)二维数组名是常量,不可以对其进行改变和赋值。而指针一般是指针变量。
3)对于第一组对应关系,a 和 p 其实都是一个指向 int [4] 数组的指针。
平常假设的 二维数组 a 先看成一个一维数组(包括 a[0]、a[1]、a[2]),然后每一个元素又是一个一维数组。
这其中的 a[0]、a[1]、a[2]其实都是假想的,在实际内存中并不存在。
所以二维数组名即是二维数组第一个元素的首地址,也即假想的 a[0] 一维数组名所表示的首地址。
4)但是第二组对应关系与第一组是截然不同的,事实上,凡是涉及到两个或更多的 * 号连用时,
或者是指针数组的形式,其中的 *b 和 p[0]、 p[1]、 p[2]、 p[3]都是在内存中真实存在的。
所以 p 这个指针本身表示的地址和 p[0] 指向的地址就会不同这是与第一组对应关系最大的不同之处。
5)两组对应关系的运算规则有很大的相似之处。可以相似记忆。
11. 关于数组名的问题
数组名并不是一个指针,也不能算作一个地址常量。因为sizeof(数组名)并不等于4。
数组名其实就是一个标签,其实并不占有内存,例如,有如下代码:
1 int a[5]; 2 int b[3][5]; 3 int* c; 4 int** d; 5 int* e[5]; 6 int(*f)[5];
编译器在遇到如果 a 这个标签时,就会自动转换为数组 a 的首元素地址。所以可以这样赋值:c = a;
同样对于二维数组的话(所谓的数组的数组),b 其实是一个数组指针,指向的是一个 int[5] 型的数组。
编译器在遇到 b 这个标签时,就会自动转换为 int[5] 型数据类型的首地址,其实就是二维数组首元素的地址。
所以可以这样赋值 f = b;
从这个意义上来讲,b[0],b[1],b[2]也可以看做是标签(也不占内存),即所谓的行向量,当编译器遇到他们的时候也会自动转换成对应的地址。
对于 e 来讲,是一个指针数组,e 首先是数组名,也是不占内存,然后e[0],e[1],e[2],e[3],e[4]实际上在内存中是存在的,这一点和 b、f 有很大不同。
e 数组中每一个元素都是指针,存储的都是地址。
编译器在遇到 e 这个标签的时候,也会自动转换成 e[0] 的地址。
所以可以这样赋值:d = e ;这里的赋值其实和 c = a;非常像,只是多了一个 * 号而已。
12. 数组名标签转换为地址赋给指针变量
关于这个问题,C++ primer中还有一个例子(涉及到了C++11新特性)。
p114:使用范围for语句处理多维数组:
1 int ia[3][4]; 2 size_t cnt = 0; 3 for(auto &row : ia) 4 for(auto &col : row) 5 { 6 col = cnt; 7 ++cnt; 8 }
在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因出事我们这么做。
举一个例子,考虑如下的循环:
1 for (const auto &row : ia) 2 for(auto col : row) 3 cout<<col<<endl;
这个循环并没有任何写操作,可是我们还是将外层循环的控制变量声明为引用类型,就是为了避免数组被自动转换为指针。
假设不用引用类型,则循环如下述形式:
1 for (auto row : ia) 2 for(auto col : row)
程序将无法通过编译。
这是因为,像之前一样第一个循环遍历 ia 的所有元素,注意这些元素实际是大小为 4 的数组。因为row不是引用类型,所以编译器初始化 row 时会自动将这些数组类型
的元素(和其他类型的数组一样)转换成指向该数组类首元素的指针,这样得到的 row 的类型就是 int * ,显然内层的循环就不合法了,编译器将试图在一个 int* 中遍历,
这显然和程序的初衷相去甚远。