按照中国大学MOOC上浙江大学翁恺老师主讲的版本所作,B站上也有资源。原课程链接如下:
https://www.icourse163.org/course/ZJU-9001
由于是大三抽空回头整理的,所以可能前五章会记的内容比较简略。此外,作为选学内容的A0:ACLLib的基本图形函数和链表两章也没有做。西电的考试是机试,理论上学到结构体就能够应付考试了,但为了以后的学习考虑建议全学。
其他各章节的链接如下:
&
运算符取得变量的地址
运算符&
scanf("%d", &i)
里的&
获得变量的地址,它的操作数必须是变量
示例:
#include
int main(void)
{
int i = 0;
// printf("0x%x\n", &i);
printf("%p\n", &i);
return 0;
}
0xbff62d6c
想让printf
输出地址应该用%p
,否则编译运行时可能会出现类型不匹配的警告
示例2:
#include
int main(void)
{
int i = 0;
int p;
p = (int)&i;
printf("0x%x\n", p);
printf("%p\n", &i);
printf("%lu\n", sizeof(int));
printf("%lu\n",sizeof(&i));
return 0;
}
0xbff62d6c
0xbff62d6c
4
4
如果不做强制将取地址得到的结果转换成int
,编译会出现类型转换警告
之前做编译时选择以32位架构编译,如果换成以64位架构编译,得
0x5c961d28
0x7fff5c961d28
4
8
可见&
可以取出一个变量的地址,地址的大小和int
是否相等取决于编译器,取决于是64位架构还是32位架构
&
不能取的地址
必须是一个明确的变量才可以取地址,&
不能对没有地址的东西取地址,如&(a+b)
,&(a++)
,&(++a)
试试这些&
&
的结果的sizeof
示例:
#include
int main(void)
{
int i = 0;
int p;
printf("%p\n", &i);
printf("%p\n", &p);
return 0;
}
0xbff62d6c
0xbff62d68
相邻整型变量i
和p
的地址刚好差4,而32位架构下int
的大小等于4B,说明这两个变量在内存中是紧挨着放的
先定义的变量内存地址更高。这两个变量都是本地变量,它们被分配在内存的堆栈(stack)中,而在堆栈里面分配变量是自顶向下分配的
示例2:
#include
int main(void)
{
int a[10];
printf("%p\n", &a);
printf("%p\n", a);
printf("%p\n", &a[0]);
printf("%p\n", &a[1]);
return 0;
}
0xbff8dd44
0xbff8dd44
0xbff8dd44
0xbff8dd48
a
的地址、直接拿a
当作地址输出、a[0]
的地址相等。相邻的数组单元之间的地址差距为4
指针变量就是记录地址的变量
指针就是保存地址的变量
示例:
int i;
int* p = &i;
*
表示p
是一个指针,指向一个int
。把i
的地址交给p
p
指向i
意思是p
里面的值是变量i
的地址
int* p,q
和int *p,q
都表示p
是一个指针,指向一个指针,而q
是普通的int
变量。换句话说我们是把*
加给p
而不是int
,*p
是一个int
,于是p
是一个指针,而并不是说p
的类型是int*
指针变量
指针变量的值是内存的地址
普通变量的值是实际的值,指针变量的值是具有实际值的变量的地址
作为参数的指针
void f(int *p)
在被调用的时候得到了某个变量的值
int i = 0; f(&i);
在函数里面可以通过这个指针访问外面的这个i
示例:
#include
void f(int *p);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
}
&i=0xbff17d70
p=0xbff17d70
访问那个地址上的变量*
*
是一个单目运算符,用来访问指针的值所表示的地址上的变量
可以做右值也可以做左值。如:int k = *p
,*p = k+1
示例:
#include
void f(int *p);
void g(int k);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
printf("*p=%d\n", *p);
*p = 26;
}
void g(int k)
{
printf("k=%d\n", k);
}
&i=0xbff17d70
p=0xbff17d70
*p=6
k=26
左值之所以叫左值,是因为出现在赋值号左边接收值的不是变量,而是值,是表达式计算的结果:a[0] = 2;
,*p = 3;
,是特殊的值,所以叫左值
为什么int i; scanf("%d",i);
编译没有报错?
正好是32位架构,整数和地址一样大,对scanf
来说把一个整数还是地址传进去没啥区别,它认为传入的是某个地址。运行时因为scanf
把读入的数字写入到不该写的地方而出错
指针应用场景一
交换两个变量的值
示例:
#include
void swap(int *pa, int *pb);
int main(void)
{
int a = 5;
int b = 6;
swap(&a, &b);
printf("a=%d,b=%d", a,b);
return 0;
}
void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
}
a=6,b=5
指针应用场景二
函数返回多个值,某些值就只能通过指针返回。传入的参数实际上是需要保存带回的结果的变量
示例:
#include
void 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=%d\n", 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];
}
}
}
min=1,max=55
指针应用场景二b
函数返回运算的状态,结果通过指针返回
常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:-1或0(在文件操作会看到大量的例子)
但是当任何数值都是有效的可能结果时,就得分开返回了。后续的语言(C++,Java)采用了异常机制来解决这个问题
示例:
#include
/**
@return 如果除法成功,返回1; 否则返回0
*/
int 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=%d\n", 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;
}
指针最常见的错误
定义了指针变量,还没有得到任何实际变量的地址之前,不能通过它去访问任何数据
为什么数组传进函数后的sizeof
不对了?
传入函数的数组成了什么?
示例:
#include
void 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)=%lu\n", sizeof(a));
printf("main a=%p\n",a);
minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
printf("a[0]=%d\n", a[0]);
printf("min=%d,max=%d\n", min, max);
int *p = &min;
printf("*p=%d\n", *p);
printf("p[0]=%d\n", p[0]);
printf("*a=%d\n", *a);
return 0;
}
void minmax(int *a, int len, int *min, int *max)
{
int i;
printf("minmax sizeof(a)=%lu\n", sizeof(a));
printf("minmax a=%p\n",a);
a[0]=1000;
*min = *max = a[0];
for ( i=1; i<len; i++ ) {
if ( a[i] < *min ) {
*min = a[i];
}
if ( a[i] > *max ) {
*max = a[i];
}
}
}
main sizeof(a)=68;
main a=0xbff0fd10
minmax sizeof(a)=4
minmax a=0xbff0fd10
a[0]=1000
min=2,max=1000
*p=2
p[0]=2
*a=1000
minmax
里的a
数组其实就是main
里的a
数组,在minmax
里可以修改a
数组
p[0]
表示认为p
所指向的是一个数组,它的第一个单元
如果将void minmax(int *a, int len, int *max, int *min);
和void minmax(int *a, int len, int *min, int *max){...}
中的int *a
改为int a[]
,编译运行后会警告对于函数参数里的数组其实是int *
函数参数表中的int a[]
样子像个数组,实际上是指针,满足sizeof(a)== sizeof(int *)
,但是可以用数组的运算符[]
进行运算
数组参数
以下四种函数原型是等价的:
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);
数组变量是特殊的指针
数组变量本身表达地址,所以
int a[10]; int *p=a;
直接用数组变量名,无需用&
取地址&
取地址a == &a[0]
[]
运算符可以对数组做,也可以对指针做:
p[0]
<==> a[0]
*
运算符可以对指针做,也可以对数组做:
*a=25;
数组变量是const
的指针,所以不能被赋值
int b[] = a;
不行,数组变量之间不能做互相赋值,作为参数传递时实际做的是int *q = a;
int b[]
<==> int * const b = ...
const
表示b
是一个常数,创建后就不能再代表别的数组
const
注:本节只适用于C99
指针与const
指针本身和所指的变量都可能是const
指针是const
表示一旦得到了某个变量的地址,值不能再被改变,不能再指向其他变量
int * const q = &i; // 指针 q 是 const
*q = 26; // OK
q++; // ERROR
所指是const
表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const
,那个变量可以被赋以别的值,指针也可以指向别的变量)
const int *p = &i;
*p = 26; // ERROR!(*p) 是 const
i=26; // OK
p=&j; // OK
这些是什么意思?
int i;
const int* p1 = &i;
int const* p2 = &i;
int *const p3 = &i;
判断哪个被const
了的标志是const
在*
的前面还是后面,const
在*
的前面表示所指是const
,const
在*
的后面表示指针是const
转换
总是可以把一个非const
的值转换成const
的
void f(const int *x);
int a = 15;
f(&a); // OK
const int b = a;
f(&b); // OK
b = a+1; // Error!
当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较小的字节数传递值给参数,又能避免函数对外面的变量的修改
const
数组
const int a[] = {1,2,3,4,5,6,};
数组变量已经是const
的指针了,这里的const
表面数组的每个单元都是const int
所以必须通过初始化进行赋值
保护数组值
因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
为了保护数组不被函数破坏,可以设置参数为const
。 如:int sum(const int a[], int length);
给一个指针加1表示要让指针指向下一个变量
如果指针不是指向一片连续的分配空间,如数组,则这种运算没有意义
示例:
#include
int main()
{
char ac[] = {0,1,2,3,4,5,6,7,8,9,};
char *p = ac;
char *p1 = &ac[5];
printf("p =%p\n", p);
printf("p+1=%p\n", p+1);
printf("*(p+1)=%d\n", *(p+1)); // *(p+n) <--> ac[n]
printf("p1-p=%d\n", p1-p);
int ai[] = {0,1,2,3,4,5,6,7,8,9,};
int *q = ai;
int *q1 = &ai[6];
printf("q =%p\n", q);
printf("q+1=%p\n", q+1);
printf("*(q+1)=%d\n", *(q+1));
printf("q1-q=%d\n", q1-q);
return 0;
}
p =0xbffbad5a
p+1=0xbffbad5b
*(p+1)=1
p1-p=5
q =0xbffbad2c
q+1=0xbffbad30
*(q+1)=1
q1-q=6
指针计算
这些算术运算可以对指针做:
+
,+=
,-
,-=
)++
/--
)
*p++
取出p
所指的那个数据来,完事之后顺便把p
移到下一个位置去
*
的优先级虽然高,但是没有++
常用于数组类的连续空间操作
在某些CPU上,这可以直接被翻译成一条汇编指令
示例:
#include
int main(void)
{
char ac[] = {0,1,2,3,4,5,6,7,8,9,-1};
char *p = &ac[0];
// for ( p=ac; *p!=-1 ; ) {
while ( *p != -1 ) {
printf("%d\n",*p++);
}
return 0;
}
0
1
2
3
4
5
6
7
8
9
指针比较
<
,<=
,==
,>
,>=
,!=
都可以对指针做
0地址
现代操作系统都是多进程操作系统,它的基本管理单元是进程。操作系统会给进程分配一个虚拟的地址空间,所有的程序在运行时都以为自己具有从0开始的一片连续空间,32位机器大小为4GB,当然实际上用不了这么多
所以任何一个程序里都有0地址,但是0地址通常是一个不能随便碰的地址,所以你的指针不应该具有0值
因此可以用0地址来表示特殊的事情:
NULL是一个预先定义的符号,表示0地址。有的编译器不愿意你用0来表示0地址
指针的类型
无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。但是指向不同类型的指针是不能直接互相赋值的,这是为了避免用错指针
指针的类型转换
void*
表示不知道指向什么东西的指针,计算时与char*
相同(但不相通)
这往往用在底层系统程序里,需要直接去访问某个内存地址所代表的一些外部设备、控制寄存器等等
指针也可以转换类型。 如:int *p = &i; void *q = (void*)p;
。这并没有改变p
所指的变量的类型,而后人用不同的眼光通过p
看它所指的变量
用指针来做什么
输入数据
如果输入数据时,先告诉你个数,然后再输入,要记录每个数据。C99可以用变量做数组定义的大小,C99之前呢?
int *a = (int*)malloc(n*sizeof(int));
申请n*sizeof(int)
个字节大小的内存。返回void *
,需要类型转换为int *
malloc
在UNIX下man malloc
需要#include
,参数类型是size_t
,可以暂时当作是int
向malloc
申请的空间的大小是以字节为单位的
返回的结果是void *
,表示指针指向一块不知道是什么的内存,需要类型转换为自己需要的类型
示例:
#include
#include
int main(void)
{
int number;
int* a;
int i;
printf("输入数量:");
scanf("%d", &number);
// int a[number];
a = (int*)malloc(number*sizeof(int));
for ( i=0; i<number; i++ ) {
scanf("%d", &a[i]);
}
for ( i=number-1; i>=0; i-- ) {
printf("%d ", a[i]);
}
free(a);
return 0;
}
没空间了?
如果申请失败则返回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位架构下运行
在返回0之前输出信息告知分配空间失败,但是程序没有终止
free()
把申请得来的空间还给”系统“
系统会记住申请的空间,申请过的空间,最终都应该要还,只能还申请来的空间的首地址
free(NULL);
没问题,0不可能是malloc
得到的地址,free
会判断如果参数是NULL
就不做处理
常见问题
申请了没free
—> 长时间运行内存逐渐下降
小程序产生的内存垃圾没有问题,操作系统有相关机制保证程序运行结束时曾经使用过的所有内存都会被清除干净
新手往往会忘了free
,而老手往往找不到合适的free
的时机
free
过了再free
地址变过了,直接去free