34

C语言之指针 - TechZone

 3 years ago
source link: http://www.techzone.ltd/post/CPointer/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C语言之指针

发布于 2020-01-12 | 分类于 C | 28分钟 | 7063字数

说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,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
请输入一个整数:1
a = 1
请再输入一个整数:2
a = 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.ltd
url的地址为:0063F804
url[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到底是不是左值呢?

如果是左值的话,那么就必须满足左值的条件。

  1. 拥有用于识别和定位一个存储位置的标识符
  2. 存储值可修改

第一点,数组名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()去测试arrayarray+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 = &num;
    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 = &num;
    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 = &num;
    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 12
num: 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点:

  1. 指针可以修改为指向不同的变量
  2. 指针可以修改为指向不同的常量
  3. 可以通过解引用来读取指针指向的数据
  4. 不可以通过解引用来修改指针指向的数据

指向非常量的常量指针

指针本身作为一种变量,也是可以修改的。因此,指针也是可以被const修饰的,只不过位置稍稍发生了点变化

...
int* const p = &num;
...
复制代码

这样的指针有如下的特性:

  1. 指针自身不能够被修改
  2. 指针指向的值可以被修改

指向常量的常量指针

在定义普通变量的时候也用const修饰,就得到了这样的指针。不过由于限制太多,一般很少用到:

...
int num = 100;
const int cnum = 200;
const int* const p = &cnum;
...
复制代码

这期博客就到此结束了,没学会的反复咀嚼,学会了的自己去实战,相信你的C语言编程能力会越来越强。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK