# ANSI C语言

<font style="background:yellow">
1

# 目录

[TOC]

# C语言学习的”临界点“

突破学校传统教学C语言桎梏的几个重要知识

  • 1、内存管理『记住内存管理的stack、heap、.bss、.data』
  • 2、指针『1级指针、2级指针作为函数的输入和输出、多级指针定理、函数指针、数组指针、常量指针、源码阅读+工程级项目接口设计』
  • 3、结构体的设计『数据结构的基础、C++封装思想的基础』
  • 4、数组和指针辨析
  • 5、C语言写错误日志『文件输入输出、传统教学不强化的点』
  • 6、Windows和Linux下的动态链接库设计『大型工程项目基础、Linux下依赖性的来源』

# 💬辨析:声明和定义

声明≠定义

  • 声明(Declaration):是指向编译器说明一个变量或函数的信息,包括:名字、类型、初始值等,即声明变量、函数的属性细节;
  • 定义(Definition):则指明变量、函数存储在哪里,当定义发生时,系统为变量或函数分配内存单元。

参考:声明(Declaration)与定义(Definition)的区别 (opens new window)

# 💬辨析:初始化和赋值

  • 初始化:在进行定义的时候,给它值是初始化
  • 赋值:定义之后,换一行或多行,才给它值,是赋值

引申到C++:

C++中在class的初始化列表中有的必须写:
1、const常量,原因是必须初始化,一旦初始化就不能修改了,也就是不能重新赋值
1
2
  • 第4节、数组的不完全初始化辅助理解上述区别
  • 只有在数组定义时,给数组值才叫初始化
  • 定义过后,再给数组值叫赋值

# ⏰C语言标准历史

参考书籍:《C Primer Plus (opens new window)

  • 本文将讲解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;
}
1
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
1

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)

-201112月,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函数的实际缓冲区大小不确定,以至于发生常见的缓冲区溢出攻击,类似的函数还有其它的。
>- 6fopen()新模式:fopen()增加了新的创建、打开模式“x”,在文件锁中比较常用。
>- 7)匿名结构体、联合体。
>- 8)多线程:头文件<threads.h>定义了创建和管理线程的函数,新的存储类修饰符_Thread_local限定了变量不能在多线程之间共享。
>- 9_Atomic类型修饰符和头文件<stdatomic.h>>- 10)改进的Unicode支持和头文件<uchar.h>>- 11quick_exit():又一种终止程序的方式,当exit()失败时用以终止程序。
>- 12)复数宏,浮点数宏。
>- 13)time.h新增timespec结构体,时间单位为纳秒,原来的timeval结构体时间单位为毫秒。 	
1
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;这样奇怪的语句,就规则强化了
1
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』说的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • switch语句判断条件可以接受的数据类型有哪些?
int
byte
char
short
都可以,但是byte不是C++的。。。Java中好像有
1
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、doublelong double等类型不允许直接进行位与操作符啊,可用间接的方法变通,如float取地址(也是&符号)转换为unsigned int类型,再用取值操作符(*),这样编译器会以为是unsigned int类型。
1
2
3
4

# 2.常用ASCII码值记忆法

  • 参考牛客网+自我思考补充

  • 或许以为这个不用记忆,但是在编码的时候,这个还是用到了,而且某些学校考试是会考的。

  • '\0' //ASCII是0

  • ' ' //空格,ASCII是32

//上面一组已经记熟了。
    
'0'//ASCII是48
'A'//ASCII是65
'a'//ASCII是97
486 59786,误久期(si ba l)......
此外,486+111=597
    
常用性质:
1、空串等于a减A,也就是'a'-'A'=32=' ' //编程的时候有人这么用
1
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,为什么却失败了呢?
1
2
  • 数组的几种初始化方式

# 3.2.完全初始化

  • 完全初始化(completely initialized):给每个元素初始化
int a[5]={0,1,2,3,4};
int a[]={1,2,3};
1
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
1
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
1
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
1
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
1
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
1
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;
1
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位。
1
2
3
4
5
6
7
8
9
  • (2)由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是不能超过8位二进位。
  • (3)位域可以无位域名,这时它只用来填充或调整位置。无名的位域是不能使用的。
struct wk
{
   int a:1;
   int :2;     //不能使用
   int b:3;
   int c:2;
}
1
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	//合法!
1
2
3

# 6.2.浮点常量

浮点常量:由整数部分,小数点,小数部分和指数部分组成

  • 1)小数形式表示时:必须包含整数部分或小数部分或两种都有
  • 2)用指数形式时:必须包含小数点或指数或两者都有
    • 带符号的指数是用e或E引入的!

合法的如下

3.145	//合法,小数形式
312E5	//合法,指数形式
31415E-5L	//合法,指数形式,而31415E-5l   也可以(注意,浮点常量和整数常量规则结合了)
312e3	//合法,指数形式
1
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");
上面,似乎输出内容一样?
    是的,但是意义不一样,而且请观察光标位置
1
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;  //编译错误,不可再次修改
1
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; //编译错误
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的指向
1
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;
/*结果产生编译警告*/
1
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的原则。
  1. const修饰p,指向的对象可变,指针的指向不可变
  • 指针常量——指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。
int a = 9;
int b = 10;
int * const p = &a;//p是一个const指针
*p = 11;    //合法,
p = &b;     //编译错误,p是一个const指针,只读,不可变
1
2
3
4
5
  1. 指针不可改变指向,指向的内容也不可变
int a = 9;
int b = 10;
const int * const p = &a;//p既是一个const指针,同时也指向了int类型的const值
*p = 11;    //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b;     //编译错误,p是一个const指针,只读,不可变
1
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);//字符串比较函数
1
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;
}
1
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数组
1
2
//b.c
extern const int ARR[];   //注意,这里不能再对ARR进行赋值
//后面可以使用ARR
1
2
3

2)第二种,在a文件中定义,并使用static修饰,b文件包含a文件,例如:

//a.h
static const int ARR[] = {0,1,2,3,4,5,6,7,8,9};  //定义int数组
1
2
//b.c
#include<a.h>
//后面可以使用ARR
注意,这里必须使用static修饰,否则多个文件包含导致编译会出现重复定义的错误。可以尝试一下。(为什么?因为,static修饰的变量,要是初始化了是放在.data段的,而不是栈区)
1
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;
}
1
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常量。

1
2
3
4
5
6
7
8
9
10

# 参考资料