8.指针


概念

  • 32位、64位,指的是CPU单次处理数据的能力,也就决定了CPU的寻址能力,由此决定了虚拟内存的位数。
  • 32位,理论的内存容量极限是4GB,指针变量大小为4个字节。
  • 64位,理论的内存容量极限是16EB,指针变量大小为8个字节。
  • (指向某一变量的)指针:某一变量的地址
  • 指针变量:存放地址的变量
  • 定义指针变量:
1
2
int a;
int *p = &a;

这里有一个int型变量,现在创建了一个指针p指向a。下面的int是指针的基类型,指针变量p里面存储了a的地址,&a即为取a的地址。

  • 由于基本上所有数据类型都不是只占一个字节的,所以指针想要指一个变量,还应该知道这个变量的长度,这由指针的基类型负责。基类型是int,那么就认为指向的变量长度4个字节,在使用的时候就有充足的信息。
  • 可以定义void *p;。void声明的指针就是不知道对应的长度,所以只能存地址用。
  • 在64位下,指针的大小全是8字节。
  • 指针的使用,必须要有实际具体的值,否则就是野指针,属于严重错误

使用

1
2
p = 0x................;//p对应地址
*p = 12;//*p对应被指向的变量本身
  • 不同基类型的指针互相赋值需要强制类型转换。

&和*

  • &表示取地址,*表示取变量的值
  • 优先级相同,3级,右结合
  • &*p就是p
  • *&a就是a

使用指针输出

  • printf()用“%p”输出地址值。
  • cout直接输出。
    注意: 基类型为char/unsigned char型的时候,要注意直接cout输出会导致按照字符串的方式输出,但是由于本意不是这样,没有尾零,后面会出一堆乱码。原因是之前说过的:字符数组相关。如果要输出char/unsigned char基类型指针存的地址,要先转换.最好是转成void *cout << (int *)(p) << endl;

指针变量的++/--依赖基类型

1
2
p++;//表示p存储的地址加上基类型的大小,比如这里int大小是4,p就会加4
p--;//表示p存储的地址减去基类型的大小,比如这里int大小是4,p就会减4
  • void基类型的指针不能++/--
  • 其他先操作再用还是先用再操作,都和之前普通变量的一样
  • *比后缀优先级低
  • *和前缀优先级相同
  • 总之都是先进行++/--,再去取变量值,而前后缀导致取到的是旧的还是新的需要再考虑

指针变量做函数参数

  • 函数的形参实参分离,在形参的改动无法影响实参。为了让函数能够操作外面的实参,可以传入指针,实现修改。但是单向传值的本质未变。
  • 借此,也可以实现函数产生多个结果并让主程序接收。但是这不是靠函数返回值,而是把需要输出的变量的指针作为参数传入函数,让函数在这上面修改,这样主程序就得到结果了。

一维数组和指针

  • 数组名就是数组第一个元素的地址。本质上a可以当作&a[0]一样使用。但是要注意a依旧是一个数组,不是一个指针,只是运用的时候“退化”为指针。sizeof(a)依然是数组所有元素的大小,&a&a[0]a的值是一样的,都是数组首地址。
  • 下标法和指针法的等效:a[i]*(a+i)是等效的,可以互用,且后者是前者的本质。而且这里的a也不一定要是数组名,只要是指针就行。也可以解释为什么6[a]的写法是正确且和a[6]相同的原因。
  • 理论上遍历数组的话,下标遍历再去a[i]不如直接指针++效率高。
  • 理论上这种方式可以将数组作为基准,前后访问到内存里的每一处。而C/C++不做越界检查,需要自己保证没有问题。
  • 指针的值可以变,相比之下,数组的地址不可以变,像a++这种写法是不行的。
  • 函数传参那里形参数组的本质是一个指针,sizeof去检查会发现是8.和普通数组名都不一样。所以传数组写形参的时候,int a[]int *a没区别。

指针涉及的计算

  • 合法的计算比较少
  • 指针+/-整型。之前的++/--算在这里
    整数乘上指针基类型大小,加或减进指针里,理解为偏移了几个这个类型的变量
  • 指针1-指针2,必须同种基类型
    地址实际差值/基类型大小,算出来是整型。理解为两个地址间差了几个这个类型的变量。
  • 此外没有任何四则运算是被允许的,什么乘除,什么指针相加。
  • 然后的话,void类型指针不允许做任何运算,上面两个也不行,因为不知道基类型大小。

字符串和指针

1
2
3
4
const char *p = "abc";

char str[] = "qwer";
const char *q = str;
  • const char *p = "abc";里,const修饰的是char,表示p指向的东西(*p)不能被修改,而p可以指向不同的地址,可以改。如果想表达这个地址是一个常量,不能指向其他地方,可以写char * const p = str; 显而易见,如果都不能改变就是const char * const p = "abc";
  • 在C++里,任何一个在源代码里写出的字符串字面量都会被加载到内存的静态数据区,是一个只读的字符数组常量,类型是const char [];,这个字符数组没有名字。这个字符数组是不能修改的,所以指向它的指针也必须加个const修饰char,否则编译报错。
  • 字符数组复制代码可以被简化为
1
2
3
4
//p1是指向源串的指针,p2是指向新串的指针
while(*p2++ = *p1++)
;
//可以做到把尾零也复制过去,因为判断前已经做了赋值
  • 指针是不可以cin输入的。

返回类型为指针的函数

  • 比如int *fun(int *x)
  • 不能返回函数内局部变量和形参的地址!因为函数返回以后,这些东西都释放了,不属于可以操作的地址。
  • 如果必须返回局部变量,那就把局部变量设成静态局部变量,不释放。但是这个行为会让程序能够从这个函数外部去访问修改这个静态局部变量。这样的做法从语言标准层面来看是完全正确的,这块空间本来就是分给我这个程序的,可以修改。但是静态局部变量的一个原则就是访问权限要控制在函数内,现在跨越了这个准则,从工程的角度这是危险的技巧。

空指针NULL

NULL是一个宏定义:#define NULL 0。专门用来给那些暂时不能确定初值的指针初始化用。否则就是个随机值,野指针,引发不知道什么的后果。这个NULL一标上去,代表这个地址是不合法的,指针还不能用。0地址是系统保护的内存地址,如果真运行了,程序会异常退出。

引用

概念,声明

  • 引用只有C++有。
  • 引用就是给变量取一个别名。
  • 引用是一种声明,不是一种定义。
  • 引用需要在声明时初始化。
  • 引用不分配单独的空间。
  • 引用声明以后,在整个生存期内都不能更改指向的变量。
1
2
int a;
int &b = a;
  • 混淆辨析
1
2
3
int a[3];//a是一个数组
int &b[3];//错误
int (&b)[3] = a;//正确

这里,int &b[3];指的是创建了一个数组,里面全是引用,而这是不行的。
int (&b)[3]指的是这个引用指向整个数组。

1
2
3
int *a;//a是一个指针
int &*b;//错误
int *&b = a;//正确

int &*b;指的是创建了一个指针,这个指针指向了一个引用,而这是不行的。
int *&b指的是这个引用指向一个指针。

  • &符号:在“定义新的变量”之前(出现在一个全新的名字之前),则代表这里不是定义变量,是在声明引用;如果出现在除此以外的地方,代表取地址符。

用处

  • 不是用来真的给你为变量取第二个名字用的,这样反而不好
  • 真正的用处是给函数传值:
1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}

int main()
{
int x = 3, y = 5;
swap(x, y);
return 0;
}

在调用的时候,自动给ab初始化,分别指向xy两个变量。因为是指代的是真实变量,swap函数里面对ab的操作就是对main函数里面xy的操作。这样子就不用像指针那种方式,形参要写取址符,函数内还要解引用。

  • 如果形参写的是个常量(字面量等),注意形参表需要加const修饰这个形参。