# ANSI C语言
<font style="background:yellow">
# 目录
[TOC]
# C语言学习的”临界点“
突破学校传统教学C语言桎梏的几个重要知识
- 1、内存管理『记住内存管理的stack、heap、.bss、.data』
- 2、指针『1级指针、2级指针作为函数的输入和输出、多级指针定理、函数指针、数组指针、常量指针、源码阅读+工程级项目接口设计』
- 3、结构体的设计『数据结构的基础、C++封装思想的基础』
- 4、数组和指针辨析
- 5、C语言写错误日志『文件输入输出、传统教学不强化的点』
- 6、Windows和Linux下的动态链接库设计『大型工程项目基础、Linux下依赖性的来源』
# 💬辨析:声明和定义
声明≠定义
- 声明(Declaration):是指向编译器说明一个变量或函数的信息,包括:名字、类型、初始值等,即声明变量、函数的属性细节;
- 定义(Definition):则指明变量、函数存储在哪里,当定义发生时,系统为变量或函数分配内存单元。
# 💬辨析:初始化和赋值
- 初始化:在进行定义的时候,给它值是初始化
- 赋值:定义之后,换一行或多行,才给它值,是赋值
引申到C++:
C++中在class的初始化列表中有的必须写:
1、const常量,原因是必须初始化,一旦初始化就不能修改了,也就是不能重新赋值
2
- 第4节、数组的不完全初始化辅助理解上述区别
- 只有在数组定义时,给数组值才叫初始化
- 定义过后,再给数组值叫赋值
# ⏰C语言标准历史
- 本文将讲解C语言各种标准,并且描述其特点。
- 用处:帮助在开发中了解编译器支持什么标准,更好的分析可能的编译出错原因,因为,有时候可能
因为当前编译器支持的标准不同
就会导致编译出错 - 各种标准细说
# 1.标准K&R C
- 1978年,丹尼斯•里奇(Dennis Ritchie)和布莱恩•柯林汉(Brian Kernighan)合作出版了《C程序设计语言》的第一版。书中介绍的C语言标准也被称作“K&R C”。
# 2.标准ANSI C、ISO C、C89、C90 (重点)
(目前,我们说的标准C指的一般就是ANSI C)
典型用书:《C程序设计语言》的第二版
- 随着C语言使用得越来越广泛,出现了许多新问题,人们日益强烈地要求对C语言进行标准化。1983年,美国国家标准协会(ANSI)组成了一个委员会,X3J11,为了创立 C 的一套标准。经过漫长而艰苦的过程,该标准于1989年完成,这个版本的语言经常被称作ANSI C,或有时称为C89(为了区别C99)。
- 在1990年,ANSI C标准(带有一些小改动)被美国国家标准协会(ANSI)采纳为ISO/IEC 9899:1990。这个版本有时候称为C90或者ISO C。
- 综上,通常情况下,我们不加非常严格的区分,ANSI C、ISO C、C89、C90可以看做是同一种标准。
传统C语言(K&R C)到 ANSI/ISO标准C语言的改进包括:
- 增加了真正的标准库
- 新的预处理命令与特性
- 函数原型允许在函数申明中指定参数类型
- 一些新的关键字,包括 const、volatile 与 signed 宽字符、宽字符串与字节多字符
- 对约定规则、声明和类型检查的许多小改动与澄清
# 3.C99标准(2000)
- 2000年3月,ANSI 采纳了 ISO/IEC 9899:1999 标准。这个标准通常指C99。
- C99新增了一些特性:
- 1)支持不定长的数组(柔性数组),即数组长度可以在运行时决定。
- 2)变量声明不必放在语句块的开头,for 语句提倡写成 for(int i=0;i<100;++i) 的形式,即i 只在for 语句块内部有效
- 3)初始化结构的时候允许对特定的元素赋值。
- 4)允许编译器化简非常数的表达式。
- 5)取消了函数返回类型默认为 int 的规定。
- Tips:但是各个公司对C99的支持所表现出来的兴趣不同。当GCC和其它一些商业编译器支持C99的大部分特性的时候,微软和Borland却似乎对此不感兴趣,他们把更多的精力放在了C++上。
典型的会造成下面情况的发生:
#include<stdio.h>
int main()
{
// C99允许在for循环内定义循环变量,而ANSIC C(C89)不允许
for(int i=0; i<10 ;++i)
{
printf("%d",i);
}
return 0;
}
2
3
4
5
6
7
8
9
10
上面的语句,在ANSI C,是不能通过编译的。因为他不支付,编译后会显示:
[Error] 'for' loop initial declarations are only allowed in C99 or C11 mode
但是,从C99开始,就可以编译通过了。
gcc下用这个方式可以指定用C99标准编译:
gcc -std=c99 test.c -o test.exe
Tips:
- 1)另外,事实上,不定长数组的定义早在C的C99标准里就已经被提出,但是从来都没在C++标准(C++98、C++03、C++11)里存在过。因此,G++支持不定长数组完全是因为它同时支持C99和C++(对C99标准支持得最好的就是G++了),而VS不怎么支持C99标准那是人尽皆知的,也就理所当然不支持C99的不定长数组了。
- 2)此外,目前没有编译器可以完全实现C99,而且为了兼容性,在写C代码时,通常我们不会去用C99标准,编译器也是默认不使用C99的,因此C语言的书里说不允许这样定义数组,也是可以理解的。而C++ primer里也这么说,那是因为它说的是事实,C++里根本就不支持不定长数组。
- 3)大概也是因为如上的原因,所以,到目前为止,我们还是认为ANSI C才是标准C。
# 4.C11标准(2011)
- 在2011年12月,ANSI 采纳了 ISO/IEC 9899:2011 标准。这个标准通常即C11,它是C程序语言的最新标准。
- 与C99相比,C11有这些变化:
>- 1)对齐处理:alignof(T)返回T的对齐方式,aligned_alloc()以指定字节和对齐方式分配内存,头文件<stdalign.h>定义了这些内容。
>- 2)_Noreturn:_Noreturn是个函数修饰符,位置在函数返回类型的前面,声明函数无返回值,有点类似于gcc的__attribute__((noreturn)),后者在声明语句尾部。
>- 3)_Generic:_Generic支持轻量级范型编程,可以把一组具有不同类型而却有相同功能的函数抽象为一个接口。
>- 4)_Static_assert():_Static_assert(),静态断言,在编译时刻进行,断言表达式必须是在编译时期可以计算的表达式,而普通的assert()在运行时刻断言。
>- 5)安全版本的几个函数:gets_s()取代了gets(),原因是后者这个I/O函数的实际缓冲区大小不确定,以至于发生常见的缓冲区溢出攻击,类似的函数还有其它的。
>- 6)fopen()新模式:fopen()增加了新的创建、打开模式“x”,在文件锁中比较常用。
>- 7)匿名结构体、联合体。
>- 8)多线程:头文件<threads.h>定义了创建和管理线程的函数,新的存储类修饰符_Thread_local限定了变量不能在多线程之间共享。
>- 9)_Atomic类型修饰符和头文件<stdatomic.h>。
>- 10)改进的Unicode支持和头文件<uchar.h>。
>- 11)quick_exit():又一种终止程序的方式,当exit()失败时用以终止程序。
>- 12)复数宏,浮点数宏。
>- 13)time.h新增timespec结构体,时间单位为纳秒,原来的timeval结构体时间单位为毫秒。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ✅语法篇
# C和C++变量命名规则探讨?🤔
变量命名规则
- 不能是C语言或者C++的标识符。
- 区分大小写
- 变量的第1个字符必须是大小写字母或者下划线。
- 即,除去第1个字符之外的其他字符是大小写字母,下划线,或数字。
疑问:为什么,第3点要求看上去这么奇怪? 为什么,不直接说,是直接由大小写字母,下划线,数字组成就好了,为何要强调第1个字符,不能是数字? 看上去,一点都不对称,没有美感
解释:
想象一下,定义这样一个变量
int 1=9;//变量名字,要是叫1,那么我们的程序,难道以后,我用1就是9?多尴尬
反观
char c='d';
char D='c';
这样的多好,原因是:
字符在C语言和C++中是有单引号''围着的
字符串是有双引号""围着的
或许,创造者在变量名取名规则的时候,首先是考虑了
1)大小写字母,毕竟是人家母语,要是计算机语言和母语由类似之处,多好。
2)数字,毕竟,阿拉伯数字享誉全球。
3)下划线,ummm,或许是为了今后的下划线命名法???疑问。。
然后发现,前面那样int 1=9;这样奇怪的语句,就规则强化了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
工程上主流的变量命名法
- 下划线命名法(C语言软件设计师常用)
- 驼峰命名法(Java软件设计师常用)
- 帕斯卡命名法
- 匈牙利名
# 1.switch语法⭐️
int
long
char
float
选D float
这个题目很好
switch相当于枚举,int long char这些整型都是又穷个数的
float有无穷多个,因此不能用float类型
『我在C++primer上面也看到,只要整型和enum』
此外,enum也不是构造类型,它是一个基本类型,
我们可以将一个enum作为switch语句的表达式!!!『C++ primer』说的
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- switch语句判断条件可以接受的数据类型有哪些?
int
byte
char
short
都可以,但是byte不是C++的。。。Java中好像有
2
3
4
5
# ⭐️类型转换?
# (1)自动类型转换
当运算符的两边出现不一致的类型时,会自动转换成较大的类型 大的意思是能表达的数的范围更大
char -> short -> int -> long -> long long
int ->float ->double
- 特殊:
- 1、对于printf,任意小于int的类型会被转换成int
- 2、但是scanf不会,要输入short,需要
%hd
『注意事项』
# (2)强制类型转换
要把一个量强制转换成另一个类型(通常是较小的类型)
注意,这时候的安全性,小的变量不总能表达大的量
- 只是从那个变量计算出了一个新的类型的值。它并不改变那个变量,无论是值还是类型都不改变
# ✔️参加位运算的数据其类型不能是?
- 牛客网传送门 (opens new window)
- 无论是float 还是double,在内存中的存储分为三部分:符号位,指数位,尾数位;位运算符对它们没有意义
网上找的:
按位运算是对字节或字中的实际位进行检测、设置或移位, 它只适用于字符型和整数型变量以及它们的变体, 对其它数据类型不适用。
loat、double、long double等类型不允许直接进行位与操作符啊,可用间接的方法变通,如float取地址(也是&符号)转换为unsigned int类型,再用取值操作符(*),这样编译器会以为是unsigned int类型。
2
3
4
# 2.常用ASCII码值记忆法
参考牛客网+自我思考补充
或许以为这个不用记忆,但是在编码的时候,这个还是用到了,而且某些学校考试是会考的。
'\0'
//ASCII是0' '
//空格,ASCII是32
//上面一组已经记熟了。
'0'//ASCII是48
'A'//ASCII是65
'a'//ASCII是97
486 597
死86,误久期(si ba l)......
此外,486+111=597
常用性质:
1、空串等于a减A,也就是'a'-'A'=32=' ' //编程的时候有人这么用
2
3
4
5
6
7
8
9
10
11
# 3.数组的『初始化方式』⭐️
- C语言和C++中数组的初始化
# 3.1.问题由来
int a[5]={0} 把数组全部初始化为0
int a[5]={1} 把数组全部初始化为1,为什么却失败了呢?
2
- 数组的几种初始化方式
# 3.2.完全初始化
完全初始化(completely initialized)
:给每个元素初始化
int a[5]={0,1,2,3,4};
int a[]={1,2,3};
2
# 2.3.完全不初始化(uninitialized)
int a[5];
注意:不进行显式初始化的情况下:
- 未初始化的全局变量以及静态变量的初始均为0(因为他们都存在.bss段,默认初始化为0)
- 未初始化的局部变量(自动变量)随机(其实也不叫随机,要是你能够精确的设计上一个释放这块内存的地方存了什么值,你就能自行控制,局部变量处于栈区,其数值是当时内存中的值。)
#include<stdio.h>
int main()
{
int bb[5];
int i=0;
for( ; i<5 ; ++i)
{
printf("%d\n" ,bb[i]);
}
return 0;
}
//output:(不确定的)
//6487736
//4202350
//4202256
//0
//29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
int main( )
{
static int bb[5];
int i=0;
for( ; i<5 ; ++i)
{
printf("%d\n" ,bb[i]);
}
return 0;
}
//output:
//0
//0
//0
//0
//0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int bb[5];
int main( )
{
int i=0;
for( ; i<5 ; ++i)
{
printf("%d\n" ,bb[i]);
}
return 0;
}
//output:
//0
//0
//0
//0
//0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.4.不完全初始化
不完全初始化(Partly initialized)
(即:部分初始化)
int a[5]={0,1,2};
K&R C语言中是这样阐述:
- 如果初始化表达式的个数比数组元素数少,则对外部变量,静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。(至于为什么是初始化为0而不是其他的,原因是.bss的实现机制)
- 如果初始化表达式的个数比数组元素数多,则是错误的。 所以,上面数组,前三个元素被初始化为0,1,2,后两个元素被初始化为0。
实际上,int a[5]={0};属于不完全初始化,先把第一个元素初始化为0,由于初始化元素个数不够,所以剩余的元素按照规则被初始化为0,虽然都是0,但是它和后面那些0的意义不同(此0非彼0)
#include<stdio.h>
int main( )
{
int bb[5]={1};
int i=0;
for( ; i<5 ; ++i)
{
printf("%d\n" ,bb[i]);
}
return 0;
}
//output:
//1
//0
//0
//0
//0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int main( )
{
static int bb[5]={1};
int i=0;
for( ; i<5 ; ++i)
{
printf("%d\n" ,bb[i]);
}
return 0;
}
//output:
//1
//0
//0
//0
//0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.5.假装初始化(我编的..)
- 其实这种初始化方式也是『不完全初始化』的一种
- 但是在ICPC中,很多人这么写
int a[10]={};
- 编译器自动将所有元素置0
# 04.union『C语言中』
又叫:共用体(联合)
吐槽:名字真多
# 1.union
union中文翻译为“联合
”(又称共用体
)
- 吐槽—没事取这么多名字干啥
...
(好吧,其实是翻译,每个人有自己的翻译方式)
- 个人倾向于用共用体的名字,因为union共用内存的特点从名字就能直观的看出来,但是也有书上写成联合,所以,我也从各种地方搜集,发现一个还算说得过去的说明,叫“联合”显示union特点的解释。
- 联合:大家联合起来使用同一个空间
# 2.union的特点
- 特点:使用同一个空间(tips:共用体也有
内存对齐
!) - 你用了,我就不能用了(除非你想使坏,把前面别人要用的给覆盖掉)
- PS:共用体,其实表明了计算机学科一个很重要的性质,那就是,其实数据都是0和1,至于为什么后面会有字符型,整型,图片,音频什么的,完全是对这些比特位的解释的不用导致的情况,所以,同样比特的可以解释是1首歌,也可以解释是一张图片(当然,需要你解释这些比特的算法正确)
- 常用场景
- 1)这么节省内存,那嵌入式必须首先用起(要是两个不会同时使用的话)
- 2)网络传输:通信中的数据包会用到共用体,因为不知道对方会发送一个什么过来,用共用体就简单了,定义几种格式的包,收到包之后就可以直接根据包的格式取出数据。
# 05.位段(也叫位域)Bit field
- 一种特殊的
结构体struct
# 1、引言
我们在做单片机的时候。微控制单元(Microcontroller Unit;MCU),又称单片微型计算机(Single Chip Microcomputer )或者单片机。经常会告诉你第几个比特是什么什么叫停止比特啥的。如何操作呢?后面的数字是说,这个成员占几个比特。其实这种手法在C语言中的实现就可以用我们现在要讲的位段方式。当然,位段并不是这些偏底层的程序实现的唯一应用场景,我们还能将这些用于网络通信等。
# 2、语法
struct test
{
unsigned int a:1;
unsigned int b:2;
unsigned int c:4;
int tt: 11;
}demo;
2
3
4
5
6
7
特点:
编译器会安排其中的位的排列,不具有可移植性
当所需的位超过一个int时会采用多个int
# 3、位段的特点和『内存对齐』
对于位域的定义,有以下几点说明:
- (1)一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
struct wy
{
unsigned a:6;
unsigned 0; //空域
unsigned b:4; //从一单元开始存放
unsigned c:4;
}
//在这个位域定义中,a占第一字节的6位,后2位填0表示不使用,
//b从第二字节开始,占用4位,c占用4位。
2
3
4
5
6
7
8
9
- (2)由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是不能超过8位二进位。
- (3)位域可以无位域名,这时它只用来填充或调整位置。无名的位域是不能使用的。
struct wk
{
int a:1;
int :2; //不能使用
int b:3;
int c:2;
}
2
3
4
5
6
7
从以上述分析可以看出,位域可以看做是一种结构类型,其特点是成员均按二进位分配
# 06.C语言常量和C++常量
如果想要真正深入学懂C语言,请使用Linux测试所有以前教材上看不懂的内容,而不是使用Windows,从本文的字符常量,就知道原因了
- 常量:为固定值,在程序执行期间不会改变
- 可以是任何的基本数据类型,如,整型数字,浮点数字,字符,字符串,布尔值
# 6.1.整数常量
- 注意:整数常量也可以带“后缀”
前缀指定“基数” | 后缀是U和L的组合 |
---|---|
1)0x或0X表示16进制 | 1)U表示无符号整数(unsigned) |
2)0表示8进制 | 2)L表示长整数(long) |
3)其余默认为10进制 | 3)U和L可大,小写(不区分!) |
4)U和L的顺序任意 |
下面都是合法的
0xFFUL //合法!
0xFFLU //合法!
0xffLU //合法!
2
3
# 6.2.浮点常量
浮点常量:由整数部分,小数点,小数部分和指数部分组成
- 1)小数形式表示时:必须包含整数部分或小数部分或两种都有
- 2)用指数形式时:必须包含小数点或指数或两者都有
- 带符号的指数是用e或E引入的!
合法的如下
3.145 //合法,小数形式
312E5 //合法,指数形式
31415E-5L //合法,指数形式,而31415E-5l 也可以(注意,浮点常量和整数常量规则结合了)
312e3 //合法,指数形式
2
3
4
# 6.3.字符常量'a'
字符常量:是括在单引号中,如果常量是以L(仅当大写时)开头,则表示它是一个宽字符常量(例如 L'x'
)
字符常量可以是:
1)一个普通的字符,例如
'x'
2)一个转义序列,例如
'\t'
3)一个通用的字符,例如
'\u02C0'
不知道这是啥?我以前做的笔记,自己咋看不懂了,,
转义序列码 | 效果 |
---|---|
'\a' | 警报铃声(打开电脑喇叭,自己听,响一下) |
'\b' | 退格键(注意,“此退格键非彼退格键”,也就是说,和我们键盘上的不一样!) |
'\f' | 换页键(请在Linux下测试这些,在Windows上,这些表现很不一样) |
‘\r’ | 回车(注意,“此回车键非彼回车键”,也就是说,和我们键盘上的不一样!) |
'\t' | |
# (1)测试'\b'
printf("aaa\b");
printf("aaa");
上面,似乎输出内容一样?
是的,但是意义不一样,而且请观察光标位置
2
3
4
代码 | 效果 |
---|---|
printf("aaa\b"); | aaa (注意,光标在最后一个a) |
printf("aaa"); | aaa光标 (注意,光标在最后一个a之后) |
测试printf("aaa\b1"); | aa1光标 (观察完上面,再测试这个,应该懂了) |
# 6.4.字符串常量"a"
# 6.5.布尔型常量
# 07.C语言static
成员
- 静态成员存在于内存,非静态成员需要实例化才会分配内存
- (注意,也就是在虚拟内存空间中,表示的是
.bss
和.data段
中) - 非静态成员的生存期决定于该类的生存期,而静态成员生存期则与程序生命期相同
# 🍀详解C语言中const
C语言中的const修饰的变量,叫做常变量(奇怪的名字)
# 1.概述:
Q:C语言中,const关键字限定一个变量为只读,但它是真正意义上的只读吗? A:C语言中的并不是的!const虽说是constant的简写,是不变的意思。 但在C语言中,const并不是说它修饰常量,而是说它限定一个变量为只读。
- C语言中,const关键字修饰的变量并非真正意义完完全全的只读!
- (难怪叫常变量,因为C语言中const就没有把这个变量变成常量!)
Tips:C++中则对const关键字进行了加强,使得真的变成了常量(详情见C++中const详解) 所以,我们常说C++中const修饰的变量,叫做const常量。
# 2.基本用法:
# (1)修饰普通变量
const int num = 10; //与int const num等价
num = 9; //编译错误,不可再次修改
2
DevC++的C语言编译器编译:
[Error] assignment of read-only variable 'NUM'
解释:
由于使用了const修饰NUM,使得NUM为只读,因此尝试对NUM再次赋值的操作是非法的,编译器将会报错。所以,如果需要使用const修饰一个变量,那么它只能在开始声明时就赋值,否则用一般做法在后面就没有机会了(比如,特别的,C语言中可以用指针间接改变)。
# (2)修饰数组
//使用const关键字修饰数组,使其元素不允许被改变
//试图修改arr的内容的操作是非法的,编译器将会报错
const int arr[] = {0,0,2,3,4}; //与int const arr[]等价
arr[2] = 1; //编译错误
2
3
4
DevC++的C语言编译:
[Error] assignment of read-only location 'arr[2]'
# (3)修饰指针(重点)
先补充
- 下面这两种写法是等效的的!
- const int a=3;
- int const a=3;
修饰指针的主要有以下几种情况: 1)const 修饰 *p,指向的对象只读,指针的指向可变:
- 常量指针——指向常量的指针,顾名思义,就是指针指向的是常量,即它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
int a = 9;
int b = 10;
const int *p = &a;//p是一个指向int类型的const值,与int const *p等价
*p = 11; //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b; //合法,改变了p的指向
2
3
4
5
换句话说更加准确:指针P是指向const int *
这里为了便于理解,可认为const修饰的是p,通常使用对指针进行解引用来访问对象,因而该对象是只读的。
- 此处补充——《C专家编程》书中的一段)
char *cp;
const char *Ccp;
ccp = Cp;
//左操作数是一个指向有const限定符的char的指针。
//右操作数是一个指向没有限定符的char的指针。
//char类型与char类型是相容的,左操作数所指向的类型具有右操作数所指向类型的限定符(无),再加上自身的限定符(const)。
//注意,反过来就不能进行赋值。试试下而的代码:
cp = ccp;
/*结果产生编译警告*/
2
3
4
5
6
7
8
9
解释:
const char *ccp;
//表示我保证,我不会去改变我指向的那个对象char *cp;
//表示我可以改变我指向的那个对象- 我们可以,想想一下一个场景来理解ccp和cp的相互赋值的合法性
- ccp=cp;//表示,cp告诉ccp说,我给你的对象,我可以改变,我赋值给你,你要是想改也可以。ccp说,我是有原则的,我的原则就是,我不会去改变我指向的那个对象(相容),ccp说,行,随你。
- cp=ccp;//表示,ccp告诉cp,我给你的对象,我可舍不得改了,我赋值给你,你可不能改,ccp说,我也是有原则的,我就要改,而它一改,编译器就阻止它说,你这样不行,侵犯了ccp的原则。
- const修饰p,指向的对象可变,指针的指向不可变:
- 指针常量——指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。
int a = 9;
int b = 10;
int * const p = &a;//p是一个const指针
*p = 11; //合法,
p = &b; //编译错误,p是一个const指针,只读,不可变
2
3
4
5
- 指针不可改变指向,指向的内容也不可变
int a = 9;
int b = 10;
const int * const p = &a;//p既是一个const指针,同时也指向了int类型的const值
*p = 11; //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b; //编译错误,p是一个const指针,只读,不可变
2
3
4
5
小结: const放在的左侧,限定了该指针指向的对象是只读的; const放在的右侧,限定了指针本身是只读的,即不可变的。
- 记忆法:
const右边修饰谁,就说明谁是不可变的
解释:
- 首先我们去掉类型说明符,查看const右边修饰的内容,上面三种情况去掉类型说明符int之后,如下:
const int *p
; //修饰*p,指针p指向的对象*p
不可变int * const p;
//修饰p,指针p本身不可变const int * const p
; //第一个修饰了*p
,第二个修饰了p
,两者都不可变- 此外,这种记忆法,还能帮助记忆先前那两个等价的形式:
- const int NUM = 10; //与int const NUM等价
- const int *p = &a; //与int const *p等价
- const int arr[] = {0,0,2,3,4}; //与int const arr[]等价
# (4)修饰函数形参(重点)
(重点,在函数接口,控制指针只做输入/in/)
实际上,为我们可以经常发现const关键字的身影,例如很多库函数的声明:
char * strncpy(char *dest, const char *src, size_t n);//字符串拷贝函数
int * strncmp(const char *s1, const char *s2, size_t n);//字符串比较函数
2
观察一个例子:
//test.c
#include<stdio.h>
void myPrint(const char *str);
void myPrint(const char *str)
{
str[0] = 'H';
printf("my print:%s\n",str);
}
int main(void)
{
char str[] = "hello world";
myPrint(str);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
此例子中,我们不希望myPrint函数修改传入的字符串内容,因此入参使用了const限定符,表明传入的字符串是只读的,因此,如果myPrint函数内部如果尝试对str进行修改,将会报错:
error: assignment of read-only location ‘*str’str[0] = 'H';
工程常用做法:
- 我们在编码过程中,如果确定传入的指针参数仅用于访问数据,那么应该将其声明为一个指向const限定类型的指针,避免函数内部对数据进行意外地修改!
# (5)修饰全局变量
我们知道,使用全局变量是一种不安全的做法,因为程序的任何部分都能够对全局数据进行修改。而如果对全局变量增加const限定符(假设该全局数据不希望被修改),就可以避免被程序其他部分修改。这里有两种使用方式。 1)第一种,在a文件中定义,其他文件中使用外部声明,例如:
//a.h
const int ARR[] = {0,1,2,3,4,5,6,7,8,9}; //定义int数组
2
//b.c
extern const int ARR[]; //注意,这里不能再对ARR进行赋值
//后面可以使用ARR
2
3
2)第二种,在a文件中定义,并使用static修饰,b文件包含a文件,例如:
//a.h
static const int ARR[] = {0,1,2,3,4,5,6,7,8,9}; //定义int数组
2
//b.c
#include<a.h>
//后面可以使用ARR
注意,这里必须使用static修饰,否则多个文件包含导致编译会出现重复定义的错误。可以尝试一下。(为什么?因为,static修饰的变量,要是初始化了是放在.data段的,而不是栈区)
2
3
4
易错点:
- Q:C语言中const修饰的变量是真正的只读吗?
- A:C语言const修饰的变量,可以被间接修改!
在C语言中,const变量只不过是修饰该变量名,它并不能使内存变为只读。也就是说,我们不能通过变量名再去操作这块内存。但是可以通过其它方法,如指针,通过指针是可以修改被const修饰的那块内存的。
#include <stdio.h>
int main(void)
{
const int a = 2018;
int *p = &a;
*p = 2019;
printf("%d\n",a);
return 0;
}
2
3
4
5
6
7
8
9
DevC++虽然有一个警告:
[Warning] initialization discards 'const' qualifier from pointer target type
([警告]初始化会从指针目标类型中丢弃'const'限定符)
但是还是可以修改!
可以看到,我们通过另外定义一个指针变量,将被const修饰的a的值改变了。
此外,对于由于像数组溢出,隐式修改等程序不规范书写造成的运行过程中的修改,编译器是无能为力的,也说明const修饰的变量仍然是具备变量属性的。
我们要知道的是,const关键字告诉了编译器,它修饰的变量不能被改变,如果代码中发现有类似改变该变量的操作,那么编译器就会捕捉这个错误。
那么它在实际中的意义之一是什么呢? 帮助程序员提前发现问题,避免不该修改的值被意外地修改,但是无法完全保证不被修改! 也就是说,C语言中const关键字是给编译器用的帮助程序员提早发现可能存在的问题。 (所以给了个warning?但是却不给error,也是够了。)
从这次的分析中,我们可以获得的思考: 1)不要忽略编译器的警告,除非你很清楚在做什么。(比如,上面我们用那些方法修改了const修饰的。) 2)虽然可以通过某种不正规途径修改const修饰的变量,但是在工程中永远不要这么做。 3)const关键字让编译器帮助我们发现变量不该被修改却被意外修改的错误。
常见的问题: C语言中:
const int maxn=5;
int test[maxn];//[Error] variably modified 'aa' at file scope
const int maxn=1000+5;
int test[maxn];
//编译出错,[Error] variably modified 'test' at file scope
//在文件范围内可变地修改了“test”,因为数组的下标只接受常量,而C语言中const修饰的变量是个“假的常量”
//当然,上面语法在C++中是可以的,因为C++对C语言中const关键字进行了加强,使得真的是常量了,
//而且,在C++中,还给const修饰而形成的常量,取名叫做const常量。
2
3
4
5
6
7
8
9
10
# 参考资料
- 《C与指针》
- 《C专家编程》
- 《C缺陷与陷阱》
- 《C Primer Plus(第6版)》,普拉达 (Stephen Prata) (opens new window)/译者: 姜佑 (opens new window)
- 『CSDN C语言讲义』王保民
- 『C语言指针详解』
- 『C语言小白变怪兽 v1.0』,严长生,C语言中文网站长