外观
03.关键字和运算符
约 4808 字大约 16 分钟
C个人随笔关键字和运算符
2020-10-20
一、const && volatile
1、const 只读变量
- const修饰的变量是只读的,本质还是变量;
- const修饰的局部变量在栈上分配空间;
- const修饰的全局变量在全局数据区分配空间;
- const只在编译期有用,在运行期无用。(const 修饰的变量不是真的常量,它只是告诉编译器该变量不能出现在赋值符号的左边。)
2、注意
- 在现代C语言编译器中,修改const全局变量将导致程序崩溃。
- 标准C语言编译器不会将const修饰的全局变量存储于只读存储区中,而是存储于可修改的全局数据区,其值依然可以改。
下面程序,在很多现代编译器中, *p = 4; 这里都会报错,而老一点的不会。
#include <stdio.h>
const int g_cc = 2;
int main()
{
const int cc = 1;
int* p = (int*)&cc;
printf("cc = %d\n", cc);
*p = 3;
printf("cc = %d\n", cc);
p = (int*)&g_cc;
printf("g_cc = %d\n", g_cc);
*p = 4;
printf("g_cc = %d\n", g_cc);
return 0;
}3、const 本质
- C语言中的const使得变量具有只读属性。
- 现代C编译器中的const将具有全局生命周期的变量存储于只读存储区。
- const不能定义真正意义上的常量。
#include <stdio.h>
const int g_array[5] = { 0 };
void modify(int* p, int v)
{
*p = v;
}
int main()
{
int const i = 0;
const static int j = 0;
int const array[5] = { 0 };
modify((int*)&i, 1); // ok
modify((int*)&j, 2); // error 具有全局生命周期的变量存储于只读存储区
modify((int*)&array[0], 3); // ok
modify((int*)&g_array[0], 4); // error 具有全局生命周期的变量存储于只读存储区
printf("i = %d\n", i);
printf("j = %d\n", j);
printf("array[0] = %d\n", array[0]);
printf("g_array[0] = %d\n", g_array[0]);
return 0;
}4、const修饰函数参数和返回值
- const修饰函数参数表示在函数体内不希望改变参数的值;
- const修饰函数返回值表示返回值不可改变,多用于返回指针的情形。
小贴士:
C语言中的字符串字面量存储于只读存储区中,在程序中需要使用const char指针。
#include <stdio.h>
const char* f(const int i)
{
i = 5; // err
return "exposed Joker";
}
int main()
{
const char* pc = f(0);
printf("%s\n", pc);
pc[6] = '_'; // err
printf("%s\n", pc);
return 0;
}5、深藏不露的volatile
- volatile可理解为“编译器警告指示字”;
- volatile告诉编译器必须每次去内存中取变量值;
- volatile主要修饰可能被多个线程访问的变量;
- volatile 也可以修饰可能被未知因数更改的变量。

6、有趣的问题
const volatile int i = 0;
一 变量i具有什么样的特性?
一 编译器如何处理这个变量?
只读变量、不可优化,每次都要去内存取值。
7、小结
- const使得变量具有只读属性
- const不能定义真正意义上的常量
- const将具有全局生命期的变量存储于只读存储区
- volatile强制编译器减少优化,必须每次从内存中取值

二、struct-union-enum-sizeof-typedef分析
1、struct 的小秘密
C语言中的struct可以看作变量的集合。
struct的问题:
一空结构体占用多大内存?
#include <stdio.h>
struct TS
{
};
int main()
{
struct TS t1;
struct TS t2;
printf("sizeof(struct TS) = %d\n", sizeof(struct TS));
printf("sizeof(t1) = %d, &t1 = %p\n", sizeof(t1), &t1);
printf("sizeof(t2) = %d, &t2 = %p\n", sizeof(t2), &t2);
return 0;
}(1)编译器出错(BCC、VC)
(2)编译器支持(GCC)
2、结构体与柔性数组
- 柔性数组即数组大小待定的数组;
- C语言中可以由结构体产生柔性数组;
- C语言中结构体的最后一个元素可以是大小未知的数组。

上面为4个字节。
SoftArray中的array仅是一个待使用的标识符,不占用存储空间。
#include <stdio.h>
#include <malloc.h>
struct SoftArray /*定义柔性数组*/
{
int len; /*柔性数组的元素数量*/
int array[]; /*柔性数组*/
};
struct SoftArray* create_soft_array(int size) /*创建柔型数组*/
{
struct SoftArray* ret = NULL;
if (size > 0)
{
ret = (struct SoftArray*)malloc(sizeof(struct SoftArray) + sizeof(int) * size);
ret->len = size;
}
return ret;
}
void delete_soft_array(struct SoftArray* sa) /*释放柔型数组*/
{
free(sa);
}
void func(struct SoftArray* sa)/*柔型数组功能测试*/
{
int i = 0;
if (NULL != sa)
{
for (i = 0; i < sa->len; i++)
{
sa->array[i] = i + 1;
}
}
}
int main()
{
int i = 0;
struct SoftArray* sa = create_soft_array(10);
func(sa);
for (i = 0; i < sa->len; i++) /*检测测试结果*/
{
printf("%d\n", sa->array[i]);
}
delete_soft_array(sa);
return 0;
}3、C语言中的union
- C语言中的union在语法上与struct相似
- union只分配最大成员的空间,所有成员共享这个空间

- union的使用受系统大小端的影响


小端系统:c.i的数据存在低位,当读取c.c时,由于c.c占一个字节,所以读取的是1。
大端系统:c.i的数据存在高位,当读取c.c时,由于c.c占一个字节,所以读取的是0。
所有变量共用一个内存,根据自身的内存大小,读写不同长短的内存。
#include <stdio.h>
int system_mode()
{
union SM
{
int i;
char c;
};
union SM sm;
sm.i = 1;
return sm.c;
}
int main()
{
printf("System Mode: %d\n", system_mode()); /*判断大小端系统*/
return 0;
}4、小结
- struct中的每个数据成员有独立的存储空间
- struct可以通过最后的数组标识符产生柔性数组
- union中的所有数据成员共享同一个存储空间
- union的使用会受到系统大小端的影响
5、枚举类型的使用方法
- enum是C语言中的一种自定义类型
- enum值是可以根据需要自定义的整型值
- 第一个定义的enum值默认为0
- 默认情况下的enum值是在前一个定义值的基础上加1
- enum类型的变量只能取定义时的离散值
6、枚举类型的特殊意义
- enum中定义的值是C语言中真正意义上的常量
- 在工程中enum多用于定义整型常量
#include <stdio.h>
enum
{
ARRAY_SIZE = 10
};
enum Color
{
RED = 0x00FF0000,
GREEN = 0x0000FF00,
BLUE = 0x000000FF
};
void PrintColor(enum Color c)
{
switch (c)
{
case RED:
printf("Color: RED (0x%08X)\n", c);
break;
case GREEN:
printf("Color: GREEN (0x%08X)\n", c);
break;
case BLUE:
printf("Color: BLUE(0x%08X)\n", c);
break;
}
}
void InitArray(int array[])
{
int i = 0;
for (i = 0; i < ARRAY_SIZE; i++)
{
array[i] = i + 1;
}
}
void PrintArray(int array[])
{
int i = 0;
for (i = 0; i < ARRAY_SIZE; i++)
{
printf("%d\n", array[i]);
}
}
int main()
{
enum Color c = GREEN;
int array[ARRAY_SIZE] = { 0 };
PrintColor(c);
InitArray(array);
PrintArray(array);
return 0;
}
7、sizeof关键字的用法
- sizeof是编译器的内置指示符
- sizeof用于计算类型或变量所占的内存大小
- sizeof的值在编译期就已经确定
sizeof用于类型:sizeof( type)
sizeof用于变量:sizeof(var)或sizeof var
8、sizeof是C语言的内置关键字而不是函数
- 在编译过程中所有的sizeof将被具体的数值所替换
- 程序的执行过程与sizeof 没有任何关系

#include <stdio.h>
int f()
{
printf("Delphi Tang\n");
return 0;
}
int main()
{
int var = 0;
int size = sizeof(var++);
printf("var = %d, size = %d\n", var, size);
size = sizeof(f());
printf("size = %d\n", size);
return 0;
}
9、typedef的意义
- typedef用于给一个已经存在的数据类型重命
- typedef本质上不能产生新的类型
- typedef重命名的类型:① 可以在typedef 语句之后定义②不能被unsigned和signed修饰
- 用法:typedef type new_name;
#include <stdio.h>
typedef int Int32; /*重命名int*/
struct _tag_point
{
int x;
int y;
};
typedef struct _tag_point Point; /*重命名结构体*/
typedef struct /*新建结构体并重命名*/
{
int length;
int array[];
} SoftArray;
typedef struct _tag_list_node ListNode;/*重命名struct _tag_list_node*/ /*可以在typedef 语句之后定义*/
struct _tag_list_node
{
ListNode* next;
};
int main()
{
Int32 i = -100; // int
//unsigned Int32 ii = 0; /* 不能被unsigned和signed修饰*/
Point p; // struct _tag_point
SoftArray* sa = NULL;
ListNode* node = NULL; // struct _tag_list_node*
return 0;
}10、小结
- enum用于定义离散值类型
- enum定义的值是真正意义上的常量
- sizeof是编译器的内置指示符
- sizeof不参与程序的执行过程
- typedef用于给类型重命名 --- 重名的类型可以在typedef 语句之后定义
三、注释符号
1、注释规则
- 编译器在编译过程中使用空格替换整个注释
- 字符串字面量中的//和/.../不代表注释符号
- /..../型注释不能被嵌套
- 注释中的**\换行符也是可以起作用的
#include <stdio.h>
int main()
{
int/*...*/i;
char* s = "abcdefgh //hijklmn";
//Is it a \
valid comment?
in/*...*/t i;
return 0;
}
2、有趣的问题
你觉得y=x/*p是什么意思?
作者本意:把×除以*p的结果赋值给y。
编译器∶将/*作为一段注释的开始,把/*后的内容都当成注释内容,直到*/出现为止。
在编译器看来,注释和其它程序元素是平等的。因此,作为工程师不能轻视注释。
3、总结
- 注释应该准确易懂,防止二义性,错误的注释有害无利
- 注释是对代码的提示,避免臃肿和喧宾夺主
- 一目了然的代码避免加注释
- 不要用缩写来注释代码,这样可能会产生误解
- 注释用于阐述原因和意图而不是描述程序的运行过程
四、接续符、转义符的使用
1、接续符的使用
- 编译器会将反斜杠剔除,跟在反斜杠后面的字符自动接续到前一行
- 在接续单词时,反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格
- 接续符适合在定义宏代码块时使用
#define SWAP(a,b) \
{ \
int temp = a; \
a = b; \
b = temp; \
}2、转义符的意义
C语言中的转义符(主要用于表示无回显字符,也可用于表示常规字符。
3、转义符的使用
当反斜杠()作为转义符使用时必须出现在单引号或者双引号之间。
#include <stdio.h>
int main()
{
char enter = '\n';
char* p = "\141\t\x62";
printf("%s", p);
printf("%c", enter);
return 0;
}
五、单引号和双引号
- C语言中的单引号用来表示字符字面量
- C语言中的双引号用来表示字符串字面量

1、下面的程序合法吗?
#include <stdio.h>
int main()
{
char* p1 = 1 ; //err
char* p2 = '1'; //err
char* p3 = "1";
printf("%s, %s, %s", p1, p2, p3);
printf('\n'); //err
printf("\n");
return 0;
}- 字符字面量被编译为对应的ASCII码
- 字符串字面量被编译为对应的内存地址
- printf 的第一个参数被当成字符串内存地址
- 内存的低地址空间不能在程序中随意访问(大于0x08048000)


2、概念混淆
#include <stdio.h>
int main()
{
char c = " ";
while( (c == "\t") || (c == " ") || (c == "\n") )
{
scanf("%c", &c);
}
return 0;
}char c = “String” 发生了什么?
分析∶
编译后字符串"string"的内存地址被赋值给变量c
内存地址占用4个字节,而变量c只占用1个字节
由于类型不同,赋值后产生截断
3、小结
- 单引号括起来的单个字符代表整数
- 双引号括起来的字符代表字符指针
- C编译器接受字符和字符串的比较,无任何意义
- C编译器允许字符串对字符变量赋值,只能得到错误
六、逻辑运算符
1、程序中的短路
- ||从左向右开始计算:
·当遇到为真的条件时停止计算,整个表达式为真·所有条件为假时表达式才为假
- &&从左向右开始计算:
·当遇到为假的条件时停止计算,整个表达式为假·所有条件为真时表达式才为真
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
int k = 0;
++i || ++j && ++k;
printf("%d\n", i);
printf("%d\n", j);
printf("%d\n", k);
return 0;
}
逻辑表达式中,&&比Ⅱ具有更高的优先级
在&&和||混合运算时,整个表达式被看作||表达式,编译器从左向右开始计算&&表达式,当某个&&表达式的值为真时,停止计算,整个表达式的值为真。
验证:
#include <stdio.h>
int g = 0;
int f()
{
printf("In f()...\n");
return g++;
}
int main()
{
if (g || f() && f())
{
printf("In if statement: %d\n", g);
}
printf("In main(): %d\n", g);
return 0;
}
2、真假转换(!碰见0返回1,否则统统返回0)
#include <stdio.h>
int main()
{
printf("%d\n", !0);
printf("%d\n", !1);
printf("%d\n", !100);
printf("%d\n", !-1000);
return 0;
}
3、小结
程序中的逻辑表达式遵从短路规则
在&&与||混合运算时∶
- 整个表达式被看作||表达式
- 从左向右先计算&&表达式
- 最后计算||表达式
逻辑非!运算符只认得0
- 碰见0返回1,否则统统返回0
只有0才代表假,其余的所有值均代表真。
七、位运算符分析
1、C语言中的位运算符
位运算符直接对 bit位进行操作,其效率最高。
2、左移和右移注意点
(1)左操作数必须为整数类型
- char和 short被隐式转换为int后进行移位操作
(2)右操作数的范围必须为∶[0,31]
(3)左移运算符<<将运算数的二进制位左移
- 规则∶高位丢弃,低位补0
(4)右移运算符>>把运算数的二进制位右移
- 规则︰高位补符号位,低位丢弃
#include <stdio.h>
int main()
{
printf("%d\n", 3 << 2);
printf("%d\n", 3 >> 1);
printf("%d\n", -1 >> 1);
printf("%d\n", 0x01 << 2 + 3);
printf("%d\n", 3 << -1); // 禁止这么做,oops!根据编译器不同会不同。
return 0;
}防错准则:
- 避免位运算符,逻辑运算符和数学运算符同时出现在一个表达式中
- 当位运算符,逻辑运算符和数学运算符需要同时参与运算时,尽量使用括号()来表达计算次序
小技巧∶
- √左移n位相当于乘以2的n次方,但效率比数学运算符高
- √右移n位相当于除以2的n次方,但效率比数学运算符高
#include <stdio.h>
#define SWAP1(a, b) \
{ \
int t = a; \
a = b; \
b = t; \
}
#define SWAP2(a, b) \
{ \
a = a + b; \
b = a - b; \
a = a - b; \
}
#define SWAP3(a, b) \
{ \
a = a ^ b; \
b = a ^ b; \
a = a ^ b; \
}
int main()
{
int a = 1;
int b = 2;
printf("a = %d\n", a);
printf("b = %d\n", b);
SWAP3(a ,b);
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}3、位运算与逻辑运算不同
- 位运算没有短路规则,每个操作数都参与运算
- 位运算的结果为整数,而不是0或1
- 位运算优先级高于逻辑运算优先级
4、小结
- 位运算符只能用于整数类型
- 左移和右移运算符的右操作数范围必须为[0,31]
- 位运算没有短路规则,所有操作数均会求值
- 位运算的效率高于四则运算和逻辑运算
- 运算优先级∶四则运算>位运算>逻辑运算
八、++,--操作符的本质
1、++和--操作符对应两条汇编指令
一前置
- 变量自增(减)1
- 取变量值
一后置
- 取变量值
- 变量自增(减)1
2、一对头疼的兄弟

#include <stdio.h>
int main()
{
int i = 0;
int r = 0;
r = (i++) + (i++) + (i++);
printf("i = %d\n", i);
printf("r = %d\n", r);
r = (++i) + (++i) + (++i);
printf("i = %d\n", i);
printf("r = %d\n", r);
return 0;
} VC GCC


- C语言中只规定了++和--对应指令的相对执行次序
- ++和--对应的汇编指令不一定连续运行
- 在混合运算中,++和--的汇编指令可能被打断执行
++和--参与混合运算结果是不确定的。
3、面试中的奇葩(编译器:贪心法)

贪心法∶++,--表达式的阅读技巧
- 一编译器处理的每个符号应该尽可能多的包含字符
- 一编译器以从左向右的顺序一个一个尽可能多的读入字符
- 一当读入的字符不可能和已读入的字符组成合法符号为止
空格可以作为C语言中一个完整符号的休止符,编译器读入空格后立即对之前读入的符号进行处理
解读:
① 第一行是会报错的
② 编译器读取顺序:读入 +
③编译器读取顺序:前面没有数,需要再读一个,假如是++呢
④ 编译器读取顺序:读入+,现在是++。接着读,要有个变量
⑤ 编译器读取顺序:读入了i,因为贪心,想接着读
⑥ 编译器读取顺序:读入了+,因为贪心接着读,而且有可能后面是变量
⑦ 编译器读取顺序:读入了+,现在是 ++i++,编译器认为程序出错了, ++i 和++读入的字符不可能和已读入的字符组成合法符号。

4、小结
- ++和--操作符在混合运算中的行为可能不同
- 编译器通过贪心法处理表达式中的子表达式
- 空格可以作为C语言中一个完整符号的休止符
- 编译器读入空格后立即对之前读入的符号进行处理
九、三目运算符和逗号表达式
1、三目运算符
三目运算符( a?b:c)可以作为逻辑运算的载体
规则:当a的值为真时,返回b的值;否则返回c的值。
以上是错的,返回的是一个值,而不是变量。以下为正确演示。

2、三目运算符的返回类型
三目运算符( a ? b: c )的返回类型
- 通过隐式类型转换规则返回b和c中的较高类型
- 当b和c不能隐式转换到同一类型时将编译出错
#include <stdio.h>
int main()
{
char c = 0;
short s = 0;
int i = 0;
double d = 0;
char* p = "str";
printf( "%d\n", sizeof(c ? c : s) );
printf( "%d\n", sizeof(i ? i : d) );
printf( "%d\n", sizeof(d ? d : p) );
return 0;
}
3、逗号表达式
- 逗号表达式是C语言中的“粘贴剂”
- 逗号表达式用于将多个子表达式连接为一个表达式
- 逗号表达式的值为最后一个子表达式的值
- 逗号表达式中的前N-1个子表达式可以没有返回值
- 逗号表达式按照从左向右的顺序计算每个子表达式的值

4、小结
- 三目运算符返回变量的值,而不是变量本身
- 三目运算符通过隐式类型转换规则确认返回值类型
- 逗号表达式按照从左向右的顺序计算每个子表达式的值
- 逗号表达式的值为最后一个子表达式的值
