1.内存和地址
我们可以把计算机内存看作是一条长街上的一排房屋。每座房子都可以容纳数据,并通过一个房号来标识。
1-1.字节(byte)-----> 8个位(bit)
计算机的内存由数以亿万计的位(bit)组成,每个位可以容纳值0或1。由于一个位所能表示的值范围有限,所以通常由许多位合成一组作为一个单位。
以下表示一个字节byte = 8位(bit),每一个空格表示一个位
0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 |
1-2.地址标识字节
每一个位置都被称为字节(byte), 每个字节可以储存无符号值 0 ~ 255 或者有符号值 -128~127
每个位置总是会包含一些值,每个字节通过地址如0x0012ff60来标识。
0x0012ff60 0x0012ff61 0x0012ff62 0x0012ff63 0x0012ff64 0x0012ff65
1-3.多个字节组合----->更大的内存单位
为了储存更大的值,我们把两个或更多的字节组合在一起作为一个更大的内存单位。
例如很多计算机以字为单位存整数,每个字一般由4个字节组成。
每一个字可以容纳的无符号整数范围 0 ~ (2^32 - 1) 或者有符号整数范围 (-2^31 - 1) ~ (2^31 - 1)
尽管一个字包含了4个字节,它仍然只有一个地址(为最左边字节的位置还是最右边字节的位置视具体计算机而定)。
边界对齐(硬件事项):在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。(小补充,不重要)
0x0012ff60 0x0012ff61 0x0012ff62 0x0012ff63
作为程序员我们应该了解:
1.内存的每个位置都由一个独一无二的地址来标识。
2.内存中的每个位置都包含一个值。
1-4.地址与内容
0x0012ff60 0x0012ff64 0x0012ff68 0x0012ff6c 0x0012ff70
112 | -1 | 1078523331 | 100 | 108 |
上面显示了5个整数,每一个都位于自己的字中。每一个字都以4个字节最左边的位置作为一个值的存储地址,你可以根据这个地址取得这个值。
但是这些地址的可读性和可记忆行太差,所以我们可以根据高级语言提供的特性之一————通过变量名而不是地址来访问内存的位置。
a b c d e
112 | -1 | 1078523331 | 0x0012ff60 | 0x0012ff64 |
注意:名字与内存之间的关联并不是硬件所提供的,是由编译器实现的。变量给了我们一种更方便的方法记住地址(但是硬件仍然通过地址访问内存)。
2.值和类型
a b c d e
112 | -1 | 1078523331 | 0x0012ff60 | 0x0012ff68 |
现在让我们来看这些位置存储的值。
a和b两个位置所存储的是整数。c位置所存储的是一个非常大的整数,d和e的位置所存储的也是整数。
下面是这些变量的声明:
int a = 112;
int b = -1;
float c = 3.14;
int* d = &a;
float* e = &c;
在这些声明中,a和b确实用于存储整型值,但c声明的是浮点型,可是上图中的c却是一个整数。
那么它到底是一个整数还是浮点数呢?
该变量包含了一序列内容为0或者1的位(bit)。这一序列内容既可以被解释为整数,也可以被解释为浮点数。
如果把c声明为整型变量,那么当程序访问他时,编译器就会产生整数型算数指令,这个值就被解释为整数。
如果把c声明为float变量,那么当程序访问他时,编译器就会产生浮点型算数指令,这个值就被解释为浮点数。
图中所标明的值是由误导性的,因为他显示了c的整型表示方式。事实上真正的浮点值时3.14。
( 如果我们以某种对float类型而言不适当的方式访问该变量时,编译器就会发出错误或警告信息)
3.指针变量的内容
a b c d e
112 | -1 | 1078523331 | 0x0012ff60 | 0x0012ff68 |
int a = 112;
int b = -1;
float c = 3.14;
int* d = &a;
float* e = &c;
3-1.指针和指针变量的定义
&a就是得到a的地址,其实“指针==地址”;
int* 指针名(如 int *d)就是定义一个整型地址(指针)变量;
变量的指针和指针变量
变量指针就是变量的存储地址,指针变量就是存储指针的变量
3-2.指针类型
char* p、short* p、int* p、float* p、double* p、struct* p
void* p 空指针类型
void表示空类型(无类型),通常应用于函数的返回值类型、函数的参数、指针类型
3-3.指针变量的初始化
d和e的内容分别是变量a与变量c的地址。(在这一点上,房屋街道这个比喻不再有效,因为房子的内容绝不可能是其他房子的地址)
4.间接访问操作符
通过一个指针访问它所指向的地址的过程称为间接访问或者解引用指针。
这个用于间接访问的操作符就是解引用操作符 * 。
int a = 112;
int b = -1;
float c = 3.14;
int* d = &a;
float* e = &c;
我们知道指针变量 d和e的内存所储存的数值分别是a和c的地址,当我们对d和e使用间接访问操作符时,表示访问该地址表示的内存位置并查看那里的值。
d是一个指向整型的指针,*d表示对d进行解引用操作得到地址 0x0012ff60,在间接访问这个地址也就是变量a指向内存位置存储的112。
5.多级指针
指针的指针——二级指针
int a = 112;
int* d = &a;
int** p = &d;
变量d是一个"指向整型的指针",则变量p是一个指向"指向整型的指针"的指针——指向指针的指针( 二级指针)。
*操作符具有从右向左的结合性,所以这个表达式相当于*(*p),我们必须从里向外逐层求值。
*p访问p所指向的地址,也就变量d。第二个间接访问操作符访问d所指向的地址,也就是变量a。
表达式 | 双重间接访问 |
a | 12 |
b | &a |
*b | a, 12 |
c | &b |
*c | b, &a |
**c | *b, a, 12 |
6.指针表达式
char ch = 'a';
char *cp = &ch;
(用ch的左值右值对图进行举例解释)
&ch
作为右值,这个表达式的值是变量ch的地址&。
cp
作为左值,表示cp所处的内存位置。
作为右值,表示cp的值,也就是ch的地址。
&cp
作为右值表示指针变量cp的地址。这个地址的类型是指向字符的指针的指针。
*cp
作为右值,表示访问变量ch地址,也就是'a'。
*cp + 1
*的优先级高于+,所以首先执行访问操作,我们可以得到ch的值也就是'a'。
我们取得这个值得一份拷贝并把它与1相加,最终结果得到'b'。(并不是一个合法得左值,只能作为右值)
*(cp + 1)
括号表达式先执行加法运算,就是把c的地址和1相加,表示ch之后的内存位置。
接着间接访问操作符*访问ch之后的内存位置。
这个表达式作为右值就是表示这个位置的值。
作为左值就是表示这个位置本身。
++cp
作为右值,表示指针增值后的一份拷贝。
也就是cp的值加1之后再返回cp值的一份拷贝。
cp++
作为右值,表示返回cp值的一份拷贝之后再增加cp的值。
*++cp
间接访问符作用于增值后的指针的拷贝上,作为右值表示ch后面那个内存地址的值,作为左值表示那个位置本身。
*cp++
作为右值表示ch的值,作为左值表示ch的内存位置。
后缀++操作符在周围的表达式中使用其原先操作数的值。
++*cp
作为右值表示cp所指向ch地址中的值增值后的一份拷贝。
(*cp)++
作为右值,ch增值前的原值。
++*++cp
首先执行++cp,结果为ch的后面的地址。
接着执行间接访问符对上面结果的拷贝进行间接访问这个未知的值。
最后再执行++操作,对这个未知的值进行增值。
++*cp++
作为右值表示cp所指向ch地址中的值增值后的一份拷贝。
然后cp自增。
总结:
1.无论是++还是*,只要记住这些操作的结合性都是从右向左,再进行逐个分析,你会发现其实并不复杂。
2.使用后缀++操作符在周围的表达式中使用其原先操作数的值。
(也就是说,后缀++只会影响后续的使用,对于当前表达式不产生影响,可以直接忽略。在当前表达式使用后,才会对其产生效果)
7.指针运算
7-1.赋值运算
int a = 1; int* p1; int* p2 p1 = &a; //使用变量的地址对指针赋值
p2 = p1; //使用指针对另一个指针赋值
p1 = 100; //使用具体地址对指针赋值
7-2.算数运算
指针加上一个整数的结果是另一个指针。
当一个指针和一个整数量执行算数运算时,整数在执行加法运算前始终会根据指针所指向类型的大小进行调整。
(也就是把指针所指向类型的大小与整数值相乘)
换句话说,如果p是一个指向char的指针,那么p + 1就是指向下一个char。如果是float类型指针,那么p+1就是指向下一个float。
7-3.关系运算
p1 > p2
表示 p1 指向的地址是否大于 p2 的地址
p1 >= p2
表示p1指向的地址是否大于p2指向的地址
(标准并未定义如果两个任意的指针进行比较会产生上面后果)
p1 == p2
表示 p1 和 p2 是否指向同一个地址
p1 != p2
表示p1和p2是否指向不同的地址
(相等与否的测试,这类比较的结果和编译器选择在任何处储存数据并无关系——两个地址要么相等要么不等)
p1 == 0
p1 != 0
表示 p1 是否为空指针
8.字符指针
const char* s = "Hello World!"; //const常量字符串:保证字符串无法被修改。
printf("%s\n", s);
printf("%c", s[0]);
sz = strlen(s);
字符指针与数组的使用方式大体上类似,但需要注意的是字符数组不能通过数组名自增操作,但是字符指针是指针,可以自增操作。
9.数组指针与指针数组
数组指针就是指向数组的指针
数据类型[*p][指针长度]
int(*p)[10] = &arr;
指针数组就是指针组成的数组
数据类型* 数组名[指针长度]
int* arr[5];
指针数组指针(了解以下就行,并尝试对其分解)
char* (*pa)[5] = &arr
pa是一个指向指针数组的指针
数组指针数组 (了解一下就行)
int(*parr[10])[5];
parr是一个数组,其中的每一个元素又是数组指针
10.函数指针与函数指针数组
函数指针是指向函数的指针
int(*pa)(int , int) = Add;
调用:
(*pa)(2, 3) 或 pa(2, 3) 或Add(2, 3)
函数指针数组(了解一下)
int(*parr[10])(2, 3)
11.空指针、NULL指针、零指针
11-1.空指针
定义p 是一个指针变量,则 p = 0; p = 0L; p = '\0'; p = 3 - 3; p = 0 * 17; 中的任何一种赋值操作之后(对于 C 来说还可以是 p = (void*)0;), p 都成为一个空指针。 (空指针不指向任何实际的对象或者函数)
11-2.NULL指针
NULL 是一个标准规定的宏定义,用来表示空指针常量。
在C语言中,NULL通常被定义为:#define NULL ((void *)0)
将指针p成为一个空指针:p = NULL;
C语言中把空指针赋给其他类型的指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
char* p = NULL;(NULL原本为void* 类型,给char*p指针赋值时发生隐式类型转化成char*类型)
int* p = NULL;
标准定义了NULL指针,作为一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为NULL,你可以给他赋一个零值。
为了测试一个指针变量是否为NULL,你可以将它与零值比较。
(就机器内部而言,NULL的实际值可能与此不同,但编译器会负责将零值与内部值之间的翻译转换)
NULL指针表示某个特定的指针并未指向任何东西。
11-3.零指针
零值指针,值为0的指针,可以时任意类型的指针,可以时通用变体类型void*,也可以时char*, int* 等等。
当一个指针类型的值为0时,认为这个指针是空的。
12.未初始化和非法指针(野指针)
——指针指向一块随机的空间,不受程序控制(垃圾内存)
12-1.野指针
野指针:访问一个已销毁或者访问受限的内存区的指针,野指针不能判断是否为NULL来避免。
12-2.野指针产生的原因
* 指针定义时未被初始化:指针被定义的时候,如果程序不对其进行初始化,它的缺省值是随机的。
* 指针被释放时没有设置为NULL:指针指向的内存空间在用free()和delete释放后未赋值NULL,如果程序员没有进行对其置空或其他赋值的操作,就会成为一个野指针。free和delete只是把指针所指向的内存给释放掉,这时指针仍然存在并且指向原来的位置,只不过这个位置的内存数据已经被释放掉了,此时这个指针指向的内存是一个垃圾内存。但由于此时指针并不是一个NULL,容易逃过校检,造成会面程序中指针访问失败。
(就好比你的亲戚搬家了或者拆迁了,你手上还留着他的旧址,然后你还想通过这个地址去找他办事)
*指针操作超越变量作用域:
在执行自增(++)操作的时候,容易指针访问越界,访问了一个不该访问的内存,结果导致程序崩溃
在临时变量的定义的代码块外,使用指针指向该临时变量的引用,当该变量释放时,指针就会变成野指针
12-3.野指针的危害
指针指向的内容已经无效了,而指针没有被置空,解引用一个非空的无效的指针是一个未被定义的行为,也就是说不一定导致错误,野指针被定位到哪里出现问题,在哪里就失效了,不好查找错误的原因。
12-4.规避方法
(指针使用之前检查其有效性)
*指针再创建的同时应进行初始化,或将它设置为NULL,或者让它指向合法的内存
*小心指针越界
*指针指向空间释放 即设置NULL