目录
3.2 const 修饰指针变量(看谁距离 const 最近)
1. sizeof 和 strlen
1.1 sizeof
sizeof 是一个计算 "变量"(参数)所占空间大小的 运算符,其参数可以是 变量、指针、数据类型、数组名、结构体、函数、对象等等,其值在编译时就已经计算好了。
为什么说 sizeof 是一个运算符,而不是一个函数呢?先来看看下面的代码:
int main(void) {
int a = 0;
printf("%d\n", sizeof a); // 4
printf("%d\n", sizeof(a)); // 4
printf("%d\n", sizeof int); // error
printf("%d\n", sizeof(int)); // 4
return 0;
}
在 32 位机上运行上述代码,发现 sizeof a 和 sizeof(a) 的计算结果均为 4,而函数一般是 fun(……) 这样表示的。
仔细想想,函数名后面没有括号合适吗?显然是不合适的,函数名后面必须要有括号,这恰恰说明了 sizeof 不是一个函数,而是一个运算符,并且 sizeof 在计算变量的空间大小时括号可以省略。
sizeof int 运行时报错,而 sizeof(int) 运行结果为 4,这说明了 sizeof 在计算数据类型大小时不能省略括号。(建议:不要省略括号)
int main(void) {
short s = 3;
int a = 10;
printf("%d\n", sizeof(s = a + 2)); // 2
printf("%d\n", sizeof(s = a)); // 2
printf("%d\n", s); // 3
return 0;
}
运行上述代码,发现 sizeof(s=a+2) 和 sizeof(s=a) 的结果是相同的,s 的值没有发生变化,这恰恰说明了 sizeof(……)内部的表达式是不参与运算的,因为在编译阶段已经为它分配好了内存。
接下来总结几点有关使用 sizeof 时的注意事项:
- sizeof(数组名),数组名表示整个数组,其求取的是整个数组的空间大小;
- &数组名,数组名表示整个数组,其取出的是整个数组的地址;
- 除了上述两种情况,其他遇到的数组名都指的是数组首元素的地址(ps:二维数组名 a 表示一维数组 a[0],也就是第 0 行的首地址)。
1.2 strlen
strlen(……) 是一个函数,程序运行过程中才能计算它的值。
其参数必须是 char * 类型(也就是必须传入地址),它用来返回字符串的实际长度。
假如一些字符存放在字符数组中,那么数组元素应该包含 '\0',否则计算 strlen 的值将会出现问题。
int main(void) {
char str[] = {'l', 'o', 'v', 'e'};
printf("%d\n", strlen(str)); // 11
return 0;
}
运行上述代码,输出结果为:
我们预期的结果是输出 4 ,为什么会输出 11 呢?这是因为 strlen 函数在计算字符串实际长度时,遇到第一个 '\0' 就会结束计算,而上述代码并没有赋给字符数组 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值,这是一个相当危险的操作。
1.3 sizeof 和 strlen 的一些计算
1.3.1 一维数组
int a[] = {1, 2, 3, 4};
printf("%d\n", sizeof(a)); // 16, sizeof(数组名) 表示计算整个数组的空间大小 4 * 4 = 16
printf("%d\n", sizeof(a + 0)); // 4, 表示数组 a 中第 0 个元素的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*a)); // 4, 表示数组 a 中第 0 个元素的值,该数组中的元素为整型,而整型占 4 个字节
printf("%d\n", sizeof(a + 1)); // 4, 表示数组 a 中第 1 个元素的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(a[1])); // 4, 表示数组 a 中第 1 个元素的值,该数组中的元素为整型,而整型占 4 个字节
printf("%d\n", sizeof(&a)); // 4, &数组名 表示整个数组的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*&a)); // 16, 表示解引用整个数组,即就是整个数组的空间大小 4 * 4 = 16
printf("%d\n", sizeof(&a + 1)); // 4, 表示移动一次整个数组的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&a[0])); // 4, 表示取第 0 个元素的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&a[0] + 1)); // 4, 表示取第 1 个元素的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
1.3.2 字符数组
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
printf("%d\n", sizeof(arr)); // 6, 没有结束符 '\0',因此值为 6
printf("%d\n", sizeof(arr + 0)); // 4, 表示第 0 个元素 (即就是 'a') 的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*arr)); // 1, 表示解引用数组 arr 的首元素地址,即就是 'a',该数组的元素类型为 char,占 1 个字节
printf("%d\n", sizeof(arr[1])); // 1, 表示数组第 1 个元素,即就是 'b',该数组的元素类型为 char,占 1 个字节
printf("%d\n", sizeof(&arr)); // 4, 表示整个数组的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&arr + 1)); // 4, 表示移动一次整个数组的地址,32 位机共有 32 根地址总线,
// 因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&arr[0] + 1)); // 4, 表示取第 1 个元素 (即就是 'b')的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", strlen(arr)); // 随机值, 表示从首元素开始,计算字符串长度,该字符数组没有 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(arr + 0)); // 随机值, 表示从首元素开始,计算字符串长度,该字符数组没有 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(*arr)); // error, 表示解引用首元素( 第 0 个元素) 地址,实际上就是首元素 'a',而传入 strlen() 函数的参数是地址,也就是传入字符 '1' 的 ASCII 值,显然这是非法的
printf("%d\n", strlen(arr[1])); // error, 表示第 1 个元素 'b',实际上传入 strlen() 函数
// 的参数是地址,也就是传入字符 'b' 的 ASCII 值,显然这是非法的
printf("%d\n", strlen(&arr)); // 随机值, 表示整个数组地址,该字符数组没有 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(&arr + 1)); // 随机值, 表示移动一次整个数组的地址,该字符数组没有 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值,与 strlen(arr) 计算出的随机值比较相差 6
printf("%d\n", strlen(&arr[0] + 1)); // 随机值, 表示从数组第一个元素( 即就是 'b' )开始,计算字符串长度,该字符数组没有 '\0',这时候它会一直寻找,直至找到 '\0'为止,因此这个结果是一个不确定的值,与 strlen(arr) 计算出的随机值比较相差 1
char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); // 7, 字符串包含 '\0',计算整个数组的大小,因此值为 7
printf("%d\n", sizeof(arr + 0)); // 4, 表示第 0 个元素的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*arr)); // 1, 表示解引用首元素的地址 (即就是 'a'),元素 'a' 的类型为 char,占用 1 个字节
printf("%d\n", sizeof(arr[1])); // 1, 表示第一个元素 (即就是 'b'),元素 'b' 的类型为 char,占用 1 个字节
printf("%d\n", sizeof(&arr)); // 4, 表示整个数组的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&arr + 1)); // 4, 表示移动一次整个数组的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&arr[0] + 1)); // 4, 表示第 1 个元素 (即就是 'b') 的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", strlen(arr)); // 6, arr 表示数组的首地址,字符串包含结束符 '\0',所以字符串长度为 6
printf("%d\n", strlen(arr + 0)); // 6, 表示第 0 个元素的地址,字符串包含结束符 '\0',所以字符串长度为 6
printf("%d\n", strlen(*arr)); // error, 表示解引用首元素 (第 0 个元素) 地址,实际上就是首元素 'a',而传入 strlen() 函数的参数是地址,也就是传入字符 'a' 的 ASCII 值,显然这是非法的
printf("%d\n", strlen(arr[1])); // error, 表示解引用第 1 个元素地址,实际上就是首元素 'b',
// 而传入 strlen() 函数的参数是地址,也就是传入字符 'b' 的 ASCII 值,显然这是非法的
printf("%d\n", strlen(&arr)); // 6, 表示整个数组的地址,字符串包含结束符 '\0',所以字符串长度为 6
printf("%d\n", strlen(&arr + 1)); // 随机值, 表示移动一次整个数组,但是这样会跳过 '\0',这时候它会一直寻找,直至找到 '\0' 为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(&arr[0] + 1)); // 5, 表示第 1 个元素的地址,也就是从 'b' 开始计算其字符串长度,所以字符串长度为 5
char *p = "abcdef";
printf("%d\n", sizeof(p)); // 4, p 是指针变量,指的是字符串的首元素 (即就是 'a') 的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(p + 1)); // 4, p + 1 指的字符串的第 1 个元素的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*p)); // 1, 表示解引用 p 的首地址,即就是 'a',char 型占用 1 个字节
printf("%d\n", sizeof(p[0])); // 1, 表示第 0 个元素 (即就是 'a'),char 型占用 1 个字节
printf("%d\n", sizeof(&p)); // 4, &p 表示指针变量的地址,p 是个二级指针,也可以理解为地址的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&p + 1)); // 4, 表示跳过一个指针变量大小的指针,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&p[0] + 1)); // 4, 表示第 1 个元素 (即就是 'b') 的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", strlen(p)); // 6, 表示从首地址开始 (即就是 'a') 计算其字符串长度,包含 '\0',因此值为 6
printf("%d\n", strlen(p + 1)); // 5, 表示从第 1 个元素地址开始 (即就是 'b') 计算其字符串长度,包含 '\0',因此值为 5
printf("%d\n", strlen(*p)); // error, *p 指的是 'a',而传入 strlen 函数的是 'a' 的地址,也就是 'a' 的 ASCII 值,这是非法的
printf("%d\n", strlen(p[0])); // error, p[0] 指的是 'a',而传入 strlen 函数的是 'a' 的地址,
// 也就是 'a' 的 ASCII 值,这是非法的
printf("%d\n", strlen(&p)); // 随机值, &p 表示存放 p 的地址,这是一个未知的值,不知道 '\0' 在哪里,因此这是一个不确定的值
printf("%d\n", strlen(&p + 1)); // 随机值, 表示跳过一个指针变量大小的指针,与上一个随机值不一定差 4,因为有可能提前出现 '\0'
printf("%d\n", strlen(&p[0] + 1)); // 5, 表示从第 1 个元素 (即就是 'b') 的地址开始,计算字符串长度为 5
1.3.3 二维数组
int a[3][4] = {0};
printf("%d\n", sizeof(a)); // 48, a 表示整个数组,所占内存空间大小为 4 * 3 * 4 = 48
printf("%d\n", sizeof(a[0][0])); // 4, 表示第 0 行第 0 列的元素,数组元素类型为整型,因此占 4 个字节
printf("%d\n", sizeof(a[0]));// 16, 表示第 0 行的数组名,实际上是有 4 个元素的一维数组,所占内存为 4 * 4 = 16
printf("%d\n", sizeof(a[0] + 1)); // 4, 表示第 0 行第 1 列的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(a + 1)); // 4, 表示第 1 行的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(&a[0] + 1)); // 4, 表示第 1 行的地址,32 位机共有 32 根地址总线,因此地址所占内存大小为 32 / 8 = 4
printf("%d\n", sizeof(*a)); // 16, 表示解引用数组第 0 行的地址,也就是 a[0] 这个一维数组的大小,因此占用内存为 4 * 4 = 16
printf("%d\n", sizeof(a[3])); // 16, 表示第 3 行的地址,sizeof() 内部不参与运算,因此值为 4 * 4 = 16,切记不要认为数组越界就不计算占用内存大小
1.3.4 函数
void Func(char str[100]) { // void Func(char *str)
return; // sizeof(str) = 4, str 是个指针变量,本质上是个地址
}
void *p = malloc(100); // sizeof(p) = 4, p 是个指针变量,本质上是个地址
2. static
在 C 语言 中的用法如下:
- static 修饰局部变量,可以改变变量的生命周期,还会修改变量的存储类型(栈区 -> 静态区),并且会保留上一次计算的值,直到程序运行结束才被销毁,但是并不会改变变量的作用域。
- static 修饰全局变量或者函数,其只能在本文件中使用,即使在别的文件中用 extern 声明也不能使用,其本质是外部链接属性(全局变量具有外部链接属性)变成了内部链接属性(被 static 修饰的变量具有内部链接属性)。
- static 修饰形式参数,形参一开始存储于栈区,作用域是整个函数,而函数只有在被调用的时候才会把实参传递给形参。假如形参被 static 修饰了,那么就会在编译的时候为其分配内存,如果该函数从未被调用,那么分配的这些内存就毫无意义,这样其实是在浪费内存,因此这是不允许的。
注意:
- static int a = 0; 在编译的过程中,通过反汇编查看,发现程序会直接跳过这段代码。
- 静态变量 或者 全局变量 如果不初始化,那么值均为 0 (.bss 段)。
- 如果初始化,则在 .data 段。
3. const
3.1 const 修饰变量
在 C 语言中,const 修饰的是常变量,具有常量的属性,同时它又可以修饰变量。
const int num = 5;
num = 10; // error,const修饰变量 num,但是 num 不能被改变,这说明 const 具有常属性
const int n = 5;
int arr[n] = {0}; // error,既然 const 具有常属性,那么测试下 n 是否可以作为数组的大小,
// 结果是否定的,这说明了 const 修饰的是变量而不是常量
3.2 const 修饰指针变量(看谁距离 const 最近)
当 const 放在 * 左边时,可以将 * 左边的数据类型(如 int)通通用笔划掉,查看 const 距离谁最近,此时只留下 const * p,说明 const 修饰的是 *p,这就表示 p 所指向的内容不能改变,而指针变量 p 是可以被改变的。
int const * p;
const int * p; // 两者等价,指针变量 p 指向的内容不能通过指针来改变,但是指针变量 p 本身可以改变
运行下面的代码,发现 *p 不可以被改变,而 p 可以被改变。
int main(void) {
int num = 10;
int n = 20;
const int * p = # // 编译报错,左值指定 const 对象
p = &n; // const 位于 * 左边时,p 可以被改变
*p = 20;
return 0;
}
当 const 放在 * 右边时,可以将 * 左边的数据类型(int) 通通用笔划掉,查看 const 距离谁最近,只留下 const p,这就表示 p 所指向的内容可以被改变,而指针变量 p 是不能被改变的。
int * const p; // 指针变量 p 不能被改变,但是指针变量指向的内容可以通过指针改变
运行下面的代码,发现 p 不可以被改变,而 *p 可以被改变。
int main(void) {
int num = 10;
int n = 20;
int * const p = #
p = &n; // const 位于 * 右边时,p 不可以被改变
*p = 20;
return 0;
}
4. volatile
使用该关键字在编译时不优化,执行时不缓存,并且被 volatile 修饰的变量只能去内存中读取,而不是在寄存器中读取。它直接存取原始内存地址,保证了内存的可见性。
除此之外,volatile 还用于多线程和多 CPU 编程。
const int num = 10;
int * p = (int *)#
*p = 20; // 即使 num 被 const 修饰,但是仍然可以通过地址去改变 num 的值,这是不安全的。
// 将这段代码保存为 test.c 文件并在 VS 中运行,num 的值会被改变,若将这段代码保存为 test.cpp 文件并运行,
// 发现 num 的值不会被改变,而如果在 Linux 中运行 .c 文件(gcc test.c -O2),num 的值不会被改变,
// 这是因为编译器不同而对其作出的不同的处理(或者说是一种优化),num 的值如果一直被使用,
// 编译器就有可能把 num 的值存放到寄存器中,便于操作,毕竟寄存器的读取效率更高一些,
// 假如这时候编译器已经把 num = 10 保存在了寄存器中,下次访问的时候会直接去寄存器中读取,
// 虽然通过指针操作改变了 num 的值为 20,但是由于不会去内存中访问它,就会造成 num 的值没有改变。
// 到底从寄存器去读取这个值还是在内存中去读取,这是一个问题?
volatile const int num = 10; // 加上 volatile 关键字后,编译器在编译时不做出优化,
// 会保证在内存中读取 num 的值,而不是在寄存器中,
// 这样保证了内存的可见性。
- 一个变量可以既是 const 又是 volatile 吗?
- 可以,比如说,只读的状态寄存器。
- 一个指针可以被 volatile 修饰吗?
- 可以,比如说,当一个中断服务子程序修改一个指向 buffer 的指针时,就可以是 volatile。