- 一、指针
- 1.1 取地址运算
- 1.2 指针
- 1.3 指针的使用
- 1.4 指针与数组
- 1.4 指针与const
- 小测验
- 二、指针运算
- 2.1 指针运算
- 2.2 动态内存分配
- 小测验
一、指针 1.1 取地址运算
运算符&就是取地址运算符,我们在第一篇博客就看到了它:scanf(“%d”, &i);。里的&的作用就是获得变量的地址,它的操作数必须是变量。为什么变量有地址?因为C语言的变量是放在内存里的,比如一个int类型的变量在内存里面要占据4个字节。 那这个地址是个什么样的值呢?我们知道地址这些东西用16进制表达比较方便,我们来看看变量i的地址。
#includeint main(void) { int i = 0; printf("0x%xn", &i); return 0; }
运行,可以看出32位和64位编译器结果如下:
我们可以看出这些变量的地址很像个整数,我们来看看将地址赋值给一个整数看看会有什么后果。
#includeint main(void) { int i = 0; int p = &i; printf("0x%xn", &i); printf("%pn", &i); return 0; }
可以看出有warning,提示我们int与int*类型不同。
如果现在我将地址强制转换成int,然后将这个int变量以16进制打印出来,看看会有什么结果。
#includeint main(void) { int i = 0; int p; p = (int)&i; printf("0x%Xn", p); printf("%pn", &i); return 0; }
32位编译器时,输出一样。
64位编译器时,作为int和作为地址输出就不一样了。
为了探索为什么不相等,我们写代码看下int和地址的大小分别是多少:
#includeint main(void) { int i = 0; int p; p = (int)&i; printf("0x%Xn", p); printf("%pn", &i); printf("%lun",sizeof(int)); printf("%lun", sizeof(&i)); return 0; }
可以看出64位编译器下,int大小是4个字节,这个地址取出来的大小是8个字节。
可以看出32位编译器下,int大小是4个字节,这个地址取出来的大小也是4个字节。
因此,地址的大小是否与int相同取决于编译器。我们使用printf打印地址时,应该使用%p。地址和整数的大小并不永远相等的,这和你的架构有关。
&必须接变量,不能对没有地址的东西取地址,比如:
• &(a+b) • &(a++) • &(++a)
我们测试一下:
#includeint main(void) { int i = 0; printf("%p",&(i++)); return 0; }
运行,结果如下:
我们试下&可不可以取以下东西的地址。
• 变量的地址 • 相邻的变量的地址 • &的结果的sizeof • 数组的地址 • 数组单元的地址 • 相邻的数组单元的地址
我们来测试相邻变量的地址:可以看出两个变量放在一起,位置相差sizeof(int)=4个字节。
这是因为变量都放在堆栈里面,而根据C语言内存模型可以知道,在堆栈里面分配内存是自顶向下。所以我们先写的变量i的地址更高,后写的变量p地址更低,但它们是紧挨着的。它们的差距就是4,这个4就是sizeof(int)。
我们再来看看对数组取地址:
#includeint main(void) { int a[10]; printf("%pn", &a); printf("%pn", a); printf("%pn", &a[0]); printf("%pn", &a[1]); return 0; }
可以看出 &a = a = &a[0]。
如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量? 答案是肯定的,比如我们用过的scanf函数,scanf(“%d”, &i);。根据scanf()的原型,我们需要一个参数能保存别的变量的地址。如何表达能够保存地址的变量?这时候就需要指针。
普通变量的值是实际的值,指针就是保存地址的变量,指针变量的值是具有实际值的变量的地址,示意图如下:
指针的用法如下:
int i; int* p = &i; int* p,q; //定义了一个指针p和一个int变量q。我们不是把*加给了int,而是把*加给了p。 int *p,q; //和上面那个一样的意思。 如果要定义两个指针,那就是int *p,*q
指针可以作为作为参数:
void f(int *p); 在被调用的时候得到了某个变量的地址: int i=0; f(&i); 在函数里面可以通过这个指针访问外面的这个i
我们通过一个例子来进行测试,看是否能在函数里面读取到外面变量的地址。
#includevoid f(int *p); int main(void) { int i = 6; printf("&i=%pn", &i); f(&i); return 0; } void f(int *p) { printf(" p=%pn", p); }
运行,可以看出在函数内部也能得到变量i的地址。
访问那个地址上的值
我通过指针读到变量的地址后,如果想改变那个变量的值,该怎么办呢?这时候需要使用 *,它是一个单目运算符,用来访问指针的值所表示的地址上的变量。
• 可以做右值也可以做左值 • int k = *p; • *p = k+1;
我们来测试一下是否可以通过*访问到指针表示的地址上的变量。
#includevoid f(int *p); int main(void) { int i = 6; printf("&i=%pn", &i); printf("i=%dn", i); f(&i); return 0; } void f(int *p) { printf(" p=%pn", p); printf(" *p=%dn", *p); }
运行,可以发现成功访问到了。
那我们能不能改变那个变量的值呢?
#includevoid f(int *p); int main(void) { int i = 6; printf("&i=%pn", &i); printf("i=%dn", i); f(&i); printf("i=%dn", i); return 0; } void f(int *p) { printf(" p=%pn", p); printf(" *p=%dn", *p); *p = 26; }
运行,可以发现i的值被改变了。
左值之所以叫左值
• 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果: • a[0] = 2; • *p = 3; • 是特殊的值,所以叫做左值
指针的运算符&和*
• 他们互相反作用 • *&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr • &*yptr -> &(*yptr) -> &(y) -> 得到y的地址,也就是yptr -> yptr
为什么int i; scanf(“%d”, i);编译没有报错?
因为i是个整数,且刚好在32位架构下,整数和地址一样大。你把整数传进去和把地址传进去,scanf没看出区别。
1.3 指针的使用
指针的应用场景1:交换两个变量的值
#includevoid swap(int *pa, int *pb); int main(void) { int a = 5; int b = 6; swap(&a, &b); printf("a=%d,b=%dn", a,b); return 0; } void swap(int *pa, int *pb) { int t = *pa; *pa = *pb; *pb = t; }
运行,可以看出a和b的值成功交换了。
指针应用场景2:函数返回多个值,某些值就只能通过指针返回。比如传入的参数实际上是需要保存带回的结果的变量。
现在我有一个数组,需要返回最大值和最小值。我可以使用两个变量保存最大值与最小值,并将两个变量的地址作为参数传入函数。在函数中通过指针访问那两个变量并修改它们的值。
#includevoid minmax(int a[], int len, int *max, int *min); int main(void) { int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55}; int min,max; minmax(a, sizeof(a)/sizeof(a[0]), &min, &max); printf("min=%d,max=%dn", min, max); return 0; } void minmax(int a[], int len, int *min, int *max) { int i; *min = *max = a[0]; for (i = 1; i < len; i++) { if (a[i] < *min) { *min = a[i]; } if (a[i] > *max) { *max = a[i]; } } }
运行,可以发现成功找到了最大值与最小值。
指针应用场景3:函数返回运算的状态,结果通过指针返回
• 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错: • -1或0(在文件操作会看到大量的例子) • 但是当任何数值都是有效的可能结果时,就得分开返回了 • 状态用函数的return来返回,实际的值通过指针参数来返回 • 后续的语言(C++,Java)采用了异常机制来解决这个问题
举例:两个整数做除法
#includeint divide(int a, int b, int *result); int main(void) { int a=5; int b=2; int c; if (divide(a, b, &c)) { printf("%d/%d=%dn",a,b,c); } return 0; } int divide(int a, int b, int *result) { int ret = 1; if (b == 0) ret = 0; else { *result = a/b; } return ret; }
可以看出,如果无法相除,就会返回0。如果成功,就会返回1,且结果通过指针保存在c中。
指针最常见的错误:定义了指针变量,还没有指向任何变量,就开始使用指针
我这里定义了一个指针,现在试着让它指向的值更改为12。
#includeint main(void) { int i = 6; int *p; int k = 12; *p = 12; return 0; }
运行,可以看出报错。
我们来分析原因:我们知道,所有的本地变量都不会有默认的初始值,如果没有对它做过赋值,这个p里面没有明确的值,可能是个乱七八糟的东西。如果把它当作地址的话,可能会指向一片莫名其妙的地方。当你使用*p=12时,你试图向那个地方写入12,而这个地方可能是不能写的。
我们来测试下,如果让 *p有个初始值为0,这个地方是肯定不能写的,现在让它写入12看看。可以看出直接中断。
当我们向函数传一个普通变量,参数接收到的是个值。如果传一个指针,参数也是接收到的一个值,这个时候的值是地址。当我们把数组作为一个值传递给函数,函数的参数表内有一个变量去接受那个数组,这个变量到底接收到了什么?我们拿上面写的minmax这个函数来做实验。
为什么在minmax函数内不能用sizeof算出这个a[]的元素个数呢? 到底它的sizeof是多少呢?我们在main函数和minmax函数中分别取打印a[]的sizeof。同时打印a[]的地址。
#includevoid minmax(int a[], int len, int *max, int *min); int main(void) { int a[] = { 1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55 }; int min, max; printf("main sizeof(a)=%lun", sizeof(a)); printf("main a=%pn", a); minmax(a, sizeof(a) / sizeof(a[0]), &min, &max); printf("min=%d,max=%dn", min, max); return 0; } void minmax(int a[], int len, int *min, int *max) { int i; printf("minmax sizeof(a)=%lun", sizeof(a)); printf("minmax a=%pn", a); *min = *max = a[0]; for (i = 1; i < len; i++) { if (a[i] < *min) { *min = a[i]; } if (a[i] > *max) { *max = a[i]; } } }
运行,可以看出在32位编译器条件下,minmax函数中a[]的大小为4,刚好为一个指针的大小。同时,minmax函数与main函数中a[]的地址一模一样,说明这两个完全是同一个数组,相当于一个演员演了一对双胞胎。
为了来测试这两个数组就是同一个,我们在minmax函数中将a[0]的值更改为1000,然后去main函数看看a[0]的值。我们运行程序,可以看出a[0[=1000。
所以,函数参数表里面的数组就是指针!这也就解释了为什么在参数表内不要在a[]的括号内写数字,为什么在函数内无法用sizeof求出a[]的大小。
既然参数表里的数组就是个指针,那我们在参数表内把它写成个指针,行不行?答案是可以的。
可以看出,我们虽然传入了一个指针,但是对它做了一系列数组的操作。看起来数组和指针似乎存在某种联系,我们有以下总结:
1、传入函数的数组成了什么?
函数参数表中的数组实际上是指针 sizeof(a) == sizeof(int*) 但是可以用数组的运算符[]进行运算
2、数组参数
以下四种函数原型是等价的: int sum(int *ar, int n); int sum(int *, int); int sum(int ar[], int n); int sum(int [], int);
3、数组变量是特殊的指针
数组变量本⾝身表达地址,所以 int a[10]; int*p=a; // 无需用&取地址 但是数组的单元表达的是变量,需要用&取地址 a == &a[0] []运算符可以对数组做,也可以对指针做: p[0] <==> a[0] *运算符可以对指针做,也可以对数组做: *a = 25; *p = a[0] = 25 数组变量是const的指针,所以不能被赋值 int b[] = a; //不可以 int *q = a; //可以 int a[] <==> int * const a 这个const加在这里告诉这个a是个常数不能被改变
1.4 指针与const
const是个修饰符,加在变量前面,告诉这个变量不能被修改。指针是一种变量,由两部分内容组成:一个是指针本身,一个是指针所指的那个变量。那么在这种情况下,当指针与const遇到了,指针本身可以是const,指针指向的那个值可以是const。他们有什么样的区别和联系呢?
1、指针是const
表示一旦得到了某个变量的地址,不能再指向其他变量 int * const q = &i; // q这个指针是 const,即q的值(i的地址)不能被改变,q指向i这个事实不能改变了。 *q = 26; // OK,通过q做一些访问并改变i这个变量的值是可以的 q++; // ERROR
2、所指是const
表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const) const int *p = &i; *p = 26; // ERROR! (*p) 是 const i = 26; //OK p = & j; //OK
3、这里有三种写法,看看什么意思
int i; const int* p1 = &i; //不能通过*p1去修改i的值 int const* p2 = &i; //不能通过*p2去修改i的值 int *const p3 = &i; //p3必须指向i,不能指向其他地方。
判断哪个被const了的标志是const在*的前面还是后面。const在后面,指针不能修改。const在前面,不能通过指针修改值。
4、转换
我们总是可以把一个非const的值转换成const的。
void f(const int* x); //代表你给我一个指针,我在我的函数内部不会去动这个指针。 int a = 15; f(&a); // ok,函数需要const int的指针,我们给了1个非const的指针,没问题。 const int b = a; f(&b); // ok b = a + 1; // Error!
这种我们拿来做什么呢?当要传递的参数的类型比地址大的时候(传一些结构体之类的),这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改。
5、const数组
数组也是一种特殊的指针,因此也可以搭配const使用。
const int a[] = {1,2,3,4,5,6,};
数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
所以必须通过初始化进行赋值
6、保护数组值
• 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值 • 为了保护数组不被函数破坏,可以设置参数为const • int sum(const int a[], int length);
小测验
1、对于:
int a[] = {5, 15, 34, 54, 14, 2, 52, 72};
int *p = &a[5];
则p[-2]的值是?
A. 编译出错,因为数组下标越界了
B. 运行出错,因为数组下标越界了
C. 54
D. 2
答案:C
2、如果:
int a[] = {0};
int *p = a;
则以下哪些表达式的结果为真?
A. p == a[0]
B. p == &a[0]
C. *p == a[0]
D. p[0] == a[0]
答案:B、C、D
3、以下变量定义:
int* p,q; 中,p和q都是指针。
正确答案:错误
4、对于:
int a[] = {5, 15, 34, 54, 14, 2, 52, 72};
int *p = &a[1];
则p[2]的值是?
答案:54
我们都知道1+1=2。但是,对于指针呢?让指针加1,结果是真正加1了吗?我们来测试一下。
#includeint main(void) { char ac[] = {1,2,3,4,5,6,7,8,9}; char *p = ac; printf("p =%pn", p); printf("p+1=%pn", p+1); return 0; }
运行,可以看出相差1。
那我们把char类型的数组更改为int类型的呢?可以看出相差4。
sizeof(char)=1,sizeof(int)=4。因此,指针加1不是让地址值加1,而是在地址值上加1个sizeof(所指向值的类型)。示意图如下所示:
因此,对于1+1这个问题:
• 给一个指针加1表示要让指针指向下一个变量 int a[10]; int *p = a; *(p+1) —> a[1] *(p+n) —> a[n] • 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义
指针计算
• 这些算术运算可以对指针做: • 给指针加、减一个整数(+, +=, -, -=) • 递增递减(++/--) • 两个指针相减
我们来看看指针相减是怎么一回事。我们分别定义一个char类型的数组和int类型的数组,用两个指针变量保存第一个元素的地址和第6个元素的地址。接着让这两个指针相减,看结果是多少。
#includeint main(void) { char ac[] = {1,2,3,4,5,6,7,8,9}; char *p = &ac[0]; char *p1 = &ac[5]; printf("p =%pn", p); printf("p1 =%pn", p1); printf("p1-p=%dn", p1-p); int ai[] = {1,2,3,4,5,6,7,8,9 }; int *q = &ai[0]; int *q1 = &ai[6]; printf("q =%pn", q); printf("q1 =%pn", q1); printf("q1-q=%dn", q1 - q); return 0; }
可以看出,两个数组相减不是得到两个地址的差值,而是还要去除以类型的大小,表示这段区域可以放多少个这样的值。
我们在程序里面经常看到*p++这个东西
• 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去 • *的优先级虽然高,但是没有++高 • 常用于数组类的连续空间操作 • 在某些CPU上,这可以直接被翻译成一条汇编指令
我们写一些代码来看看*p++的用法。
#includeint main(void) { char ac[] = {1,2,3,4,5,6,7,8,9, -1}; char *p = ac; int i; //原始的方法遍历数组 for (i = 0; i < sizeof(ac) / sizeof(ac[0]); i++) { printf("%d ", ac[i]); } printf("n"); //使用*p++的方法遍历数组 while (*p != -1) { printf("%d ", *p++); } printf("n"); return 0; }
指针比较
<, <=, ==, >, >=, != 都可以对指针做 比较它们在内存中的地址 数组中的单元的地址肯定是线性递增的
0地址
现在的操作系统都是多进程的操作系统,它的基本管理单元叫做进程。什么是进程?比如你运行了一个浏览器,那就是个进程。我们打开了Visual Studio进行编程,Visual Studio也是个进程。操作系统会给每个进程一些虚拟的空间,所有的程序在运行时都以为自己有一片从0开始的连续的空间。因此,任何程序都有0地址,但是这个0地址不能碰,有的甚至都不能读。
• 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址 • 所以你的指针不应该具有0值 • 因此可以用0地址来表示特殊的事情: • 返回的指针是无效的 • 指针没有被真正初始化(先初始化为0) • NULL是一个预定义的符号,表示0地址 • 有的编译器不愿意你用0来表示0地址
指针的类型
指针是有类型的,不同类型的指针不能互相赋值。我们来举个例子,有个char类型的指针和int类型的指针,现在将char类型指针赋值给int类型的指针,看会有什么后果。
#includeint main(void) { char ac[] = {1,2,3,4,5,6,7,8,9,-1}; char *p = ac; int ai[] = {1,2,3,4,5,6,7,8,9,-1}; int *q = ai; q=p; return 0; }
运行,可以看出不报错,但是有warning。
这样做会导致一些不好的后果。如果我将指针p赋值给q,现在我让*q=0,按理说应该把0赋值给ac[0]。但实际上会让ac[0]、ac[1]、ac[2]、ac[3]都赋值为0。
指针赋值总结如下:
• 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址 • 但是指向不同类型的指针是不能直接互相赋值的 • 这是为了避免用错指针
指针的类型转换
如果我就想做指针类型转换怎么办呢?实际上是可以做的,不过不要乱用,一般在malloc的时候搭配void*使用。
• void* 表示不知道指向什么东西的指针 • 计算时与char*相同(但不相通) • 指针也可以转换类型 • int *p = &i; void*q = (void*)p; • 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量 • 我不再当你是int啦,我认为你就是个void!
总结:指针的作用
用指针来做什么 • 需要传入较大的数据时用作参数 • 传入数组后对数组做操作 • 函数返回不止一个结果 • 需要用函数来修改不止一个变量 • 动态申请的内存...
2.2 动态内存分配
之前我们讲过,如果输入数据时,先告诉你个数,然后再输入,要记录每个数据。C99可以用变量做数组定义的大小,C99之前呢?那就只能用动态内存分配,如下面一行所示:
int *a = (int*)malloc(n*sizeof(int));
我们来看看malloc的定义:
• 使用前需要包含stdlib.h这个头文件: #include• malloc函数原型为: void* malloc(size_t size); • 向malloc申请的空间的大小是以字节为单位的 • 返回的结果是void*,需要类型转换为自己需要的类型 • (int*)malloc(n*sizeof(int))
我们写出代码来看看malloc如何工作的:
#include#include int main(void) { int number; int *a; int i; printf("输入数量:"); scanf_s("%d", &number); //申请一片number*sizeof(int)个字节的内存,然后进行类型转换 //现在我们可以将a当作int类型的数组使用了 a=(int*)malloc(number*sizeof(int)); for (i = 0; i < number; i++) { scanf_s("%d", &a[i]); } for (i = number - 1; i >= 0; i--) { printf("%d ", a[i]); } //这片内存是借的,用完需要还 free(a); return 0; }
运行,可以看到成功分配了内存,并倒序打印了数组中的值。
如果malloc申请时没空间了怎么办?如果申请失败则返回0,或者叫做NULL。你的系统能给你多大的空间?我们写出代码来看下。
#include#include int main(void) { void *p; int cnt = 0; while ((p = malloc(100 * 1024 * 1024))) { cnt++; } printf("分配了%d00MB的空间n", cnt); return 0; }
在32位编译平台下,分配了1900MB。
在64位编译平台下,分配内存45G。
free()
• 把申请得来的空间还给“系统” • 申请过的空间,最终都应该要还 • 混出来的,迟早都是要还的 • 只能还申请来的空间的首地址 • free(0)?
我们申请一段内存,然后将首地址++,并释放该地址,看会发生什么。
#include#include int main(void) { char *p; int cnt = 0; p = malloc(100 * 1024 * 1024); p++; free(p); return 0; }
可以看出,直接抛出异常。
我们再来看看释放一个不是申请来的内存看看有什么结果。
#include#include int main(void) { int *p; int i; p = &i; free(p); return 0; }
可以看出也抛出异常。
如果我们free(NULL)不会出错。
这是因为NULL就是0地址,0地址不可能是个有效的地址,它不可能是malloc来的。free也是一个函数,如果给它一个NULL,那它就不做事情。只是,有什么必要做这件事情呢?这是因为良好习惯就是:有一个指针出来了,我们先初始化为0。如果由于某些原因我们没有去malloc分配一片内存给它或者malloc得到一个失败的结果,我们去free那个指针没问题。
常见问题
• 申请了没free—>长时间运行内存逐渐下降 • 新手:忘了 • 老手:找不到合适的free的时机 如果程序小,基本没影响,程序结束后内存被释放。如果程序很大,就会造成严重后果。 • free过了再free • 地址变过了,直接去free
小测验
1、对于以下代码段,正确的说法是:
char *p;
while (1) {
p = malloc(1);
*p = 0;
}
A. 最终程序会因为没有没有空间了而退出
B. 最终程序会因为向0地址写入而退出
C. 程序会一直运行下去
D. 程序不能被编译
答案:B
2、对于以下代码段:
int a[] = {1,2,3,4,5,};
int *p = a;
int *q = &a[5];
printf("%d", q-p);
当sizeof(int)为4时,以下说法正确的是:
A. 因为第三行的错误不能编译
B. 因为第三行的错误运行时崩溃
C. 输出5
D. 输出20
答案:C
3、使用malloc就可以做出运行时可以随时改变大小的数组
答案:错误



