搞懂C语言指针,看这篇就够了!
说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,可以说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。
在讲指针之前,我们先来了解下变量在「内存」中是如何存放的。
在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。
通过变量名来访问变量,是一种「相对安全」的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。
那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节
,那么你其实可以把内存比作一个超级大的「字符型数组」。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址
。每个地址可以存放「1字节」的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。
那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。
初识指针和指针变量
那么我们现在就来切入正题,指针是个什么东西呢?
所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不一样的是,指针变量存储的是「地址」。
定义指针
指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢:
很简单:类型名* 指针变量名
char* pa;//定义一个字符变量的指针,名称为pa
int* pb;//定义一个整型变量的指针,名称为pb
float* pc;//定义一个浮点型变量的指针,名称为pc
注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。
取地址运算符和取值运算符
获取某个变量的地址,使用取地址运算符&
,如:
char* pa = &a;int* pb = &f;
如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*
,如:
printf('%c, %d\n', *pa, *pb);
这里你可能发现,定义指针的时候也使用了*
,这里属于符号的「重用」,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示「定义一个指针变量」,在其他的时候则用来「获取指针变量指向的变量的值」。
直接通过变量名来访问变量的值称之为直接访问
,通过指针这样的形式访问称之为间接访问
,因此取值运算符有时候也成为「间接运算符」。
比如:
//Example 01//代码来源于网络,非个人原创#include <stdio.h>int main(void){ char a = 'f'; int f = 123; char* pa = &a; int* pf = &f;
printf('a = %c\n', *pa); printf('f = %d\n', *pf);
*pa = 'c'; *pf += 1;
printf('now, a = %c\n', *pa); printf('now, f = %d\n', *pf);
printf('sizeof pa = %d\n', sizeof(pa)); printf('sizeof pf = %d\n', sizeof(pf));
printf('the addr of a is: %p\n', pa); printf('the addr of f is: %p\n', pf);
return 0;}
程序实现如下:
//Consequence 01
a = f
f = 123
now, a = c
now, f = 124
sizeof pa = 4
sizeof pf = 4
the addr of a is: 00EFF97F
the addr of f is: 00EFF970
避免访问未初始化的指针
void f(){ int* a; *a = 10;}
像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个「随机值」。但是如果是在指针里面,那么就有可能覆盖到「其他的内存区域」,甚至可能是系统正在使用的「关键区域」,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被「中止」并「报错」。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被「莫名其妙地修改」,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。
指针和数组
有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。
数组的地址
我们刚刚说了,指针实际上就是变量在「内存中的地址」,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢?
我们知道,从标准输入流中读取一个值到变量中,用的是scanf
函数,一般貌似在后面都要加上&
,这个其实就是我们刚刚说的「取地址运算符」。如果你存储的位置是指针变量的话,那就不需要。
//Example 02
int main(void)
{
int a;
int* p = &a;
printf('请输入一个整数:');
scanf('%d', &a);//此处需要&
printf('a = %d\n', a);
printf('请再输入一个整数:');
scanf('%d', p);//此处不需要&
printf('a = %d\n', a);
return 0;
}
程序运行如下:
//Consequence 02请输入一个整数:1a = 1请再输入一个整数:2a = 2
在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要&
来取地址完成这个任务。而对于指针变量来说,本身就是「另外一个」普通变量的「地址信息」,因此直接给出指针的值就可以了。
试想一下,我们在使用scanf
函数的时候,是不是也有不需要使用&
的时候?就是在读取「字符串」的时候:
//Example 03
#include <stdio.h>
int main(void)
{
char url[100];
url[99] = '\0';
printf('请输入TechZone的域名:');
scanf('%s', url);//此处也不用&
printf('你输入的域名是:%s\n', url);
return 0;
}
程序执行如下:
//Consequence 03请输入TechZone的域名:www.techzone.ltd你输入的域名是:www.techzone.ltd
因此很好推理:数组名其实就是一个「地址信息」,实际上就是数组「第一个元素的地址」。咱们试试把第一个元素的地址和数组的地址做个对比就知道了:
//Example 03 V2
#include <stdio.h>
int main(void)
{
char url[100];
printf('请输入TechZone的域名:');
url[99] = '\0';
scanf('%s', url);
printf('你输入的域名是:%s\n', url);
printf('url的地址为:%p\n', url);
printf('url[0]的地址为:%p\n', &url[0]);
if (url == &url[0])
{
printf('两者一致!');
}
else
{
printf('两者不一致!');
}
return 0;
}
程序运行结果为:
//Comsequense 03 V2请输入TechZone的域名:www.techzone.ltd你输入的域名是:www.techzone.ltdurl的地址为:0063F804url[0]的地址为:0063F804两者一致!
这么看,应该是实锤了。那么数组后面的元素也就是依次往后放置,有兴趣的也可以自己写代码尝试把它们输出看看。
指向数组的指针
刚刚我们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针自然也就有两种定义的方法:
...
char* p;
//方法1
p = a;
//方法2
p = &a[0];
指针的运算
当指针指向数组元素的时候,可以对指针变量进行「加减」运算,+n
表示指向p指针所指向的元素的「下n个元素」,-n
表示指向p指针所指向的元素的「上n个元素」。并不是将地址加1。
如:
//Example 04#include <stdio.h>int main(void){ int a[] = { 1,2,3,4,5 }; int* p = a; printf('*p = %d, *(p+1) = %d, *(p+2) = %d\n', *p, *(p + 1), *(p + 2)); printf('*p -> %p, *(p+1) -> %p, *(p+2) -> %p\n', p, p + 1, p + 2); return 0;}
执行结果如下:
//Consequence 04
*p = 1, *(p+1) = 2, *(p+2) = 3
*p -> 00AFF838, *(p+1) -> 00AFF83C, *(p+2) -> 00AFF840
有的小伙伴可能会想,编译器是怎么知道访问下一个元素而不是地址直接加1呢?
其实就在我们定义指针变量的时候,就已经告诉编译器了。如果我们定义的是整型数组的指针,那么指针加1,实际上就是加上一个sizeof(int)
的距离。相对于标准的下标访问,使用指针来间接访问数组元素的方法叫做指针法
。
其实使用指针法来访问数组的元素,不一定需要定义一个指向数组的单独的指针变量,因为数组名自身就是指向数组「第一个元素」的指针,因此指针法可以直接作用于数组名:
...printf('p -> %p, p+1 -> %p, p+2 -> %p\n', a, a+1, a+2);printf('a = %d, a+1 = %d, a+2 = %d', *a, *(a+1), *(a+2));...
执行结果如下:
p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF840
b = 1, b+1 = 2, b+2 = 3
现在你是不是感觉,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然非常像,但是绝对「不是」一种东西。
甚至你还可以直接用指针来定义字符串,然后用下标法来读取每一个元素:
//Example 05//代码来源于网络#include <stdio.h>#include <string.h>int main(void){ char* str = 'I love TechZone!'; int i, length;
length = strlen(str);
for (i = 0; i < length, i++) { printf('%c', str[i]); } printf('\n');
return 0;}
程序运行如下:
//Consequence 05
I love TechZone!
在刚刚的代码里面,我们定义了一个「字符指针」变量,并且初始化成指向一个字符串。后来的操作,不仅在它身上可以使用「字符串处理函数」,还可以用「下标法」访问字符串中的每一个字符。
当然,循环部分这样写也是没毛病的:
...for (i = 0, i < length, i++){ printf('%c', *(str + i));}
这就相当于利用了指针法来读取。
指针和数组的区别
刚刚说了许多指针和数组相互替换的例子,可能有的小伙伴又开始说:“这俩货不就是一个东西吗?”
随着你对指针和数组越来越了解,你会发现,C语言的创始人不会这么无聊去创建两种一样的东西,还叫上不同的名字。指针和数组终究是「不一样」的。
比如笔者之前看过的一个例子:
//Example 06
//代码来源于网络
#include <stdio.h>
int main(void)
{
char str[] = 'I love TechZone!';
int count = 0;
while (*str++ != '\0')
{
count++;
}
printf('总共有%d个字符。\n', count);
return 0;
}
当编译器报错的时候,你可能会开始怀疑你学了假的C语言语法:
//Error in Example 06错误(活动)E0137表达式必须是可修改的左值错误C2105“++”需要左值
我们知道,*str++ != ‘\0’
是一个复合表达式,那么就要遵循「运算符优先级」来看。具体可以回顾《C语言运算符优先级及ASCII对照表》。
str++
比*str
的优先级「更高」,但是自增运算符要在「下一条语句」的时候才能生效。所以这个语句的理解就是,先取出str
所指向的值,判断是否为\0
,若是,则跳出循环,然后str
指向下一个字符的位置。
看上去貌似没啥毛病,但是,看看编译器告诉我们的东西:表达式必须是可修改的左值
++
的操作对象是str
,那么str
到底是不是「左值」呢?
如果是左值的话,那么就必须满足左值的条件。
❝
拥有用于识别和定位一个存储位置的标识符 存储值可修改 ❞
第一点,数组名str
是可以满足的,因为数组名实际上就是定位数组第一个元素的位置。但是第二点就不满足了,数组名实际上是一个地址,地址是「不可以」修改的,它是一个常量。如果非要利用上面的思路来实现的话,可以将代码改成这样:
//Example 06 V2
//代码来源于网络
#include <stdio.h>
int main(void)
{
char str[] = 'I love TechZone!';
char* target = str;
int count = 0;
while (*target++ != '\0')
{
count++;
}
printf('总共有%d个字符。\n', count);
return 0;
}
这样就可以正常执行了:
//Consequence 06 V2总共有16个字符。
这样我们就可以得出:数组名只是一个「地址」,而指针是一个「左值」。
指针数组?数组指针?
看下面的例子,你能分辨出哪个是指针数组,哪个是数组指针吗?
int* p1[5];
int(*p2)[5];
单个的我们都可以判断,但是组合起来就有些难度了。
答案:
int* p1[5];//指针数组int(*p2)[5];//数组指针
我们挨个来分析。
指针数组
数组下标[]
的优先级是最高的,因此p1
是一个有5个元素的「数组」。那么这个数组的类型是什么呢?答案就是int*
,是「指向整型变量的指针」。因此这是一个「指针数组」。
那么这样的数组应该怎么样去初始化呢?
你可以定义5个变量,然后挨个取地址来初始化。
不过这样太繁琐了,但是,并不是说指针数组就没什么用。
比如:
//Example 07
#include <stdio.h>
int main(void)
{
char* p1[5] = {
'人生苦短,我用Python。',
'PHP是世界上最好的语言!',
'One more thing...',
'一个好的程序员应该是那种过单行线都要往两边看的人。',
'C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。'
};
int i;
for (i = 0; i < 5; i++)
{
printf('%s\n', p1[i]);
}
return 0;
}
结果如下:
//Consequence 07人生苦短,我用Python。PHP是世界上最好的语言!One more thing...一个好的程序员应该是那种过单行线都要往两边看的人。C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。
这样是不是比二维数组来的更加直接更加通俗呢?
数组指针
()
和[]
在优先级里面属于「同级」,那么就按照「先后顺序」进行。
int(*p2)
将p2
定义为「指针」, 后面跟随着一个5个元素的「数组」,p2
就指向这个数组。因此,数组指针是一个「指针」,它指向的是一个数组。
但是,如果想对数组指针初始化的时候,千万要小心,比如:
//Example 08
#include <stdio.h>
int main(void)
{
int(*p2)[5] = {1, 2, 3, 4, 5};
int i;
for (i = 0; i < 5; i++)
{
printf('%d\n', *(p2 + i));
}
return 0;
}
Visual Studio 2019报出以下的错误:
//Error and Warning in Example 08错误(活动)E0146初始值设定项值太多错误C2440“初始化”: 无法从“initializer list”转换为“int (*)[5]”警告C4477“printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”
这其实是一个非常典型的错误使用指针的案例,编译器提示说这里有一个「整数」赋值给「指针变量」的问题,因为p2
归根结底还是指针,所以应该给它传递一个「地址」才行,更改一下:
//Example 08 V2
#include <stdio.h>
int main(void)
{
int temp[5] = {1, 2, 3, 4, 5};
int(*p2)[5] = temp;
int i;
for (i = 0; i < 5; i++)
{
printf('%d\n', *(p2 + i));
}
return 0;
}
//Error and Warning in Example 08 V2错误(活动)E0144'int *' 类型的值不能用于初始化 'int (*)[5]' 类型的实体错误C2440“初始化”: 无法从“int [5]”转换为“int (*)[5]”警告C4477“printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”
可是怎么还是有问题呢?
我们回顾一下,指针是如何指向数组的。
int temp[5] = {1, 2, 3, 4, 5};
int* p = temp;
我们原本以为,指针p
是指向数组的指针,但是实际上「并不是」。仔细想想就会发现,这个指针实际上是指向的数组的「第一个元素」,而不是指向数组。因为数组里面的元素在内存中都是挨着个儿存放的,因此只需要知道第一个元素的地址,就可以访问到后面的所有元素。
但是,这么来看的话,指针p
指向的就是一个「整型变量」的指针,并不是指向「数组」的指针。而刚刚我们用的数组指针,才是指向数组的指针。因此,应该将「数组的地址」传递给数组指针,而不是将第一个元素的地址传入,尽管它们值相同,但是「含义」确实不一样:
//Example 08 V3//Example 08 V2#include <stdio.h>int main(void){ int temp[5] = {1, 2, 3, 4, 5}; int(*p2)[5] = &temp;//此处取地址 int i; for (i = 0; i < 5; i++) { printf('%d\n', *(*p2 + i)); } return 0;}
程序运行如下:
//Consequence 08
1
2
3
4
5
指针和二维数组
在上一节《C语言之数组》我们讲过「二维数组」的概念,并且我们也知道,C语言的二维数组其实在内存中也是「线性存放」的。
假设我们定义了:int array[4][5]
array
array作为数组的名称,显然应该表示的是数组的「首地址」。由于二维数组实际上就是一维数组的「线性拓展」,因此array应该就是指的指向包含5个元素的数组的指针
。
如果你用sizeof()
去测试array
和array+1
的话,就可以测试出来这样的结论。
*(array+1)
首先从刚刚的问题我们可以得出,array+1
同样也是指的指向包含5个元素的数组的指针
,因此*(array+1)
就是相当于array[1]
,而这刚好相当于array[1][0]
的数组名。因此*(array+1)
就是指第二行子数组的第一个元素的地址。
*(*(array+1)+2)
有了刚刚的结论,我们就不难推理出,这个实际上就是array[1][2]
。是不是感觉非常简单呢?
总结一下,就是下面的这些结论,记住就好,理解那当然更好:
*(array + i) == array[i]*(*(array + i) + j) == array[i][j]*(*(*(array + i) + j) + k) == array[i][j][k]...
数组指针和二维数组
我们在上一节里面讲过,在初始化二维数组的时候是可以偷懒的:
int array[][3] = {
{1, 2, 3},
{4, 5, 6}
};
刚刚我们又说过,定义一个数组指针是这样的:
int(*p)[3];
那么组合起来是什么意思呢?
int(*p)[3] = array;
通过刚刚的说明,我们可以知道,array
是指向一个3个元素的数组的「指针」,所以这里完全可以将array
的值赋值给p
。
其实C语言的指针非常灵活,同样的代码用不同的角度去解读,就可以有不同的应用。
那么如何使用指针来访问二维数组呢?没错,就是使用「数组指针」:
//Example 09#include <stdio.h>int main(void){ int array[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; int(*p)[4]; int i, j; p = array; for (i = 0, i < 3, i++) { for (j = 0, j < 4, j++) { printf('%2d ', *(*(p+i) + j)); } printf('\n'); } return 0;}
运行结果:
//Consequence 09
0 1 2 3
4 5 6 7
8 9 10 11
void指针
void
实际上是无类型
的意思。如果你尝试用它来定义一个变量,编译器肯定会「报错」,因为不同类型所占用的内存有可能「不一样」。但是如果定义的是一个指针,那就没问题。void
类型中指针可以指向「任何一个类型」的数据,也就是说,任何类型的指针都可以赋值给void
指针。
将任何类型的指针转换为void
是没有问题的。但是如果你要反过来,那就需要「强制类型转换」。此外,不要对void
指针「直接解引用」,因为编译器其实并不知道void
指针会存放什么样的类型。
//Example 10#include <stdio.h>int main(void){ int num = 1024; int* pi = # char* ps = 'TechZone'; void* pv;
pv = pi; printf('pi:%p,pv:%p\n', pi, pv); printf('*pv:%d\n', *pv);
pv = ps; printf('ps:%p,pv:%p\n', ps, pv); printf('*pv:%s\n', *pv);}
这样会报错:
//Error in Example 10
错误C2100非法的间接寻址
错误C2100非法的间接寻址
如果一定要这么做,那么可以用「强制类型转换」:
//Example 10 V2#include <stdio.h>int main(void){ int num = 1024; int* pi = # char* ps = 'TechZone'; void* pv;
pv = pi; printf('pi:%p,pv:%p\n', pi, pv); printf('*pv:%d\n', *(int*)pv);
pv = ps; printf('ps:%p,pv:%p\n', ps, pv); printf('*pv:%s\n', pv);}
当然,使用void指针一定要小心,由于void指针几乎可以「通吃」所有类型,所以间接使得不同类型的指针转换变得合法,如果代码中存在不合理的转换,编译器也不会报错。
因此,void指针能不用则不用,后面讲函数的时候,还可以解锁更多新的玩法。
NULL指针
在C语言中,如果一个指针不指向任何数据,那么就称之为「空指针」,用「NULL」来表示。NULL其实是一个宏定义:
#define NULL ((void *)0)
在大部分的操作系统中,地址0通常是一个「不被使用」的地址,所以如果一个指针指向NULL,就意味着不指向任何东西。为什么一个指针要指向NULL呢?
其实这反而是一种比较指的推荐的「编程风格」——当你暂时还不知道该指向哪儿的时候,就让它指向NULL,以后不会有太多的麻烦,比如:
//Example 11#include <stdio.h>int main(void){ int* p1; int* p2 = NULL; printf('%d\n', *p1); printf('%d\n', *p2); return 0;}
第一个指针未被初始化。在有的编译器里面,这样未初始化的变量就会被赋予「随机值」。这样指针被称为「迷途指针」,「野指针」或者「悬空指针」。如果后面的代码对这类指针解引用,而这个地址又刚好是合法的话,那么就会产生莫名其妙的结果,甚至导致程序的崩溃。因此养成良好的习惯,在暂时不清楚的情况下使用NULL,可以节省大量的后期调试的时间。
指向指针的指针
开始套娃了。其实只要你理解了指针的概念,也就没什么大不了的。
//Example 12
#include <stdio.h>
int main(void)
{
int num = 1;
int* p = #
int** pp = &p;
printf('num: %d\n', num);
printf('*p: %d\n', *p);
printf('**p: %d\n', **pp);
printf('&p: %p, pp: %p\n', &p, pp);
printf('&num: %p, p: %p, *pp: %p\n', &num, p, *pp);
return 0;
}
程序结果如下:
//Consequence 12num: 1*p: 1**p: 1&p: 004FF960, pp: 004FF960&num: 004FF96C, p: 004FF96C, *pp: 004FF96C
当然你也可以无限地套娃,一直指下去。不过这样会让代码可读性变得「很差」,过段时间可能你自己都看不懂你写的代码了。
指针数组和指向指针的指针
那么,指向指针的指针有什么用呢?
它可不是为了去创造混乱代码,在一个经典的实例里面,就可以体会到它的用处:
char* Books[] = {
'《C专家编程》',
'《C和指针》',
'《C的陷阱与缺陷》',
'《C Primer Plus》',
'《Python基础教程(第三版)》'
};
然后我们需要将这些书进行分类。我们发现,其中有一本是写Python的,其他都是C语言的。这时候指向指针的指针就派上用场了。首先,我们刚刚定义了一个指针数组,也就是说,里面的所有元素的类型「都是指针」,而数组名却又可以用指针的形式来「访问」,因此就可以使用「指向指针的指针」来指向指针数组:
...char** Python;char** CLang[4];
Python = &Books[5];CLang[0] = &Books[0];CLang[1] = &Books[1];CLang[2] = &Books[2];CLang[3] = &Books[3];...
因为字符串的取地址值实际上就是其「首地址」,也就是一个「指向字符指针的指针」,所以可以这样赋值。
这样,我们就利用指向指针的指针完成了对书籍的分类,这样既避免了浪费多余的内存,而且当其中的书名要修改,只需要改一次即可,代码的灵活性和安全性都得到了提升。
常量和指针
常量,在我们目前的认知里面,应该是这样的:
520, 'a'
或者是这样的:
#define MAX 1000#define B 'b'
常量和变量最大的区别,就是前者「不能够被修改」,后者可以。那么在C语言中,可以将变量变成像具有常量一样的特性,利用const
即可。
const int max = 1000;
const char a = 'a';
在const
关键字的作用下,变量就会「失去」本来具有的可修改的特性,变成“只读”的属性。
指向常量的指针
强大的指针当然也是可以指向被const
修饰过的变量,但这就意味着「不能通过」指针来修改它所引用的值。总结一下,就是以下4点:
❝
指针可以修改为指向不同的变量 指针可以修改为指向不同的常量 可以通过解引用来读取指针指向的数据 不可以通过解引用来修改指针指向的数据 ❞
常量指针
指向非常量的常量指针
指针本身作为一种「变量」,也是可以修改的。因此,指针也是可以被const
修饰的,只不过位置稍稍「发生了点变化」:
...int* const p = #...
这样的指针有如下的特性:
❝
指针自身不能够被修改 指针指向的值可以被修改 ❞
指向常量的常量指针
在定义普通变量的时候也用const
修饰,就得到了这样的指针。不过由于限制太多,一般很少用到:
...
int num = 100;
const int cnum = 200;
const int* const p = &cnum;
...