复习C语言,记录一些学习笔记。
本笔记基于《C Primer Plus》提炼而来,仅作为学习笔记使用。
关键字#
volatile#
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据
如果不使用这个关键字,编译器可能会将他优化,若该变量在前后两次读取之间没有被操作,他便会从缓存中直接命中,而不去读取原来的内存位置
在操作寄存器地址的时候,必须使用该关键字!
restrict#
使用 restrict 关键字声明指针的时候,需要确保该指针是访问该内存的唯一方式,允许编译器进行一定优化。
const#
int sum(const int ar[], int n); // 函数原型
int sum(const int ar[], int n) {
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
以上代码中的const是用来提示在sum函数中ar这个数组不能被修改,用来保护采用指针调用时的数组对象。如果在函数中出现了 ar[i]++ 这种修改数组的行为,编译器就会报错。
这里的const不是用来表示该数组是常量的,只是表示在函数中不可修改。
const 可以创建const数组、const指针和指向const的指针。
const int days[MONTHS] = {31, 28, 31}; // cosnt 数组
// days[0] = 28; // 报错
代表常量int数组,无法修改。
int numbers[] = {1, 2, 3};
const int * ptr = numbers; // 指向const的指针
*ptr = 2; // 不允许
ptr[1] = 1; // 不允许
numbers[0] = 2; // 允许,数组本身不是const
ptr++; // 允许,可以指向别处
代表指向const的指针,它仅表示所指向地址中的东西不可被修改。
const数据、非const数据的地址可以赋值给指向const的指针。
const数据的地址无法赋值给普通指针。
int rate = {1,2,3}
int * const ptr = rate; // ptr 指向数组开始
ptr = &rate[2]; // 不允许,该指针不能指向别处
*ptr = 12; // 允许,可以更改指向地址的内容
此处const初始化了一个不能修改指向的指针
const double * const pc = rates;
此处指针使用了两次const,既不能修改指向,也不能修改地址所在的值。
static#
静态指的是变量的地址不变,而不是值不变。
int fade = 1;
static int stay = 1;
这两条声明很像,如果写入函数里:
- fade 在每次函数调用的时候都会重新声明并初始化。
- stay 在编译该函数时就被初始化完成,单步调试时可见这一行被跳过了,stay不会被重新初始化。这条声明只是告诉编译器只有在该函数中才可看见stay变量。
形参中不能使用static关键字。
此关键字放在函数前表示函数只能在该文件中被调用。
特殊一点的运算符#
条件运算符 ?:#
一种三元运算符,就是一个简单的if-else语句,可以让代码更加简洁紧凑,例子:
x = (y < 0) ? -y : y; // 计算绝对值
如果y小于0,那么x=-y,否则x=y
逗号运算符 ,#
逗号运算符扩展了 for 循环的灵活性,看以下例程:
int main() {
const int FIRST_OZ = 46;
const int NEXT_OZ = 20;
int ounces, cost;
printf(" ounces cost\n");
for (ounces = 1, cost = FIRST_OZ; ounces <=16; ounces++, cost += NEXT_OZ) // 此处采用了 逗号运算符
printf("%5d $%4.2f\n", ounces, cost /100.0);
return 0;
}
可见,它扩展了 for 循环本来只能执行一句话的地方。
逗号表达式的值是逗号右侧项的值,看以下例程:
x = (y = 3, (z = ++y + 2) + 5);
x 最后等于 11
这句话先把3赋值给y,递增y为4,然后把4+2赋值给z,接着加5,最后把11结果赋值给x。
再看一个例子:
price = 123, 456;
price 等于 123
此句话被解析成了逗号表达式,相当于
price = 123;
456;
但是这句
price = (123, 456);
price 等于 456
因为右侧的(123, 456)被解析成了逗号表达式
间接运算符 *#
地址运算符: * 后面一般跟一个指针名称或者地址, * 给出储存在指针地址上的值
* 和指针名称之间的空格可有可无,一般在声明时使用空格隔开,在解引用变量时省略空格:
int * ptr; // 声明
a = *ptr; // 解引用
求值顺序#
与python类似,C语言对逻辑表达式的求值顺序是从左往右。一旦发现有使得整个表达式结果为假的因素,立即停止求值。
函数、数组、指针#
关于函数形参中的指针,只有在函数原型或函数定义头中才可以使用int a[]替换int * a
int sum(int a[], int n); // 函数原型
int sum(int * a[], int nn) {
// ...
}
复合字面量#
// c99
int diva[2] = {10, 20}; // 普通数组
(int [2]){10, 20}; // 复合字面量
(int []){10, 20}; // 编译器自动计算个数
复合字面量在声明的同时就要使用,是数组类型的直观表达常量。
它的优势是可以直接使用,不必再次创建数组。
储存类型#
作用域#
- 块作用域 花括号、循环体c99
- 函数作用域
- 函数原型作用域 仅限形参名
- 文件作用域 也称全局变量
链接#
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接
giants变量其他文件可调用,dodgers是该文件私有,文件内函数可调用。
静态 static 是指在内存中的地址不变,而不是值不变。
储存类别的选择#
保护性程序法则:“按需知道”原则。减少全局变量的使用。
malloc()#
malloc()需要配合free()使用。
函数原型在 stdlib.h 中
结构以及其他数据#
struct 结构#
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
}/*此处*/;
建立结构声明
book是可选标记
此处 若填写东西,则直接等同于下面的声明。
struct book library;
这里的library被声明为一个使用book结构布局的结构变量。
struct book library = {
"Book Title",
"Author",
1.92
}
初始化可以如上所示
library.value;
&library.value;
可以使用.来访问结构体成员,他是一个对象,所以可以使用&获取他的地址(.的优先级比&高)
struct guy * him;
声明结构体指针
him = &barney; //初始化
barney.income == (*him).income == him->income // 指针访问结构体
所以可得以上 恒等式(.的优先级比*高)
Caution
看清楚是 结构体 还是 结构体指针 !!!! 😵💫不要搞错了!!!
为了追求运行效率常常使用结构指针作为函数参数,防止原始数据被修改,使用 const 关键字。
直接传递结构体本身,是处理小型结构更常用的写法。
struct 的复合字面量#
(struct book) {"Title", "Author", 6.66}
union 联合#
能在同一个内存空间中储存不同的数据类型(不同时)
union hold {
int digit;
double bigfl;
char letter;
};
union hold fit; // 联合变量
union hold save[10]; // 10个联合变量的数组
union hold * pu; // 指向联合的指针
该例子中成员占用最大空间的是 double ,所以 save 数组内涵10个元素,每个都是 double
一个联合中只能存储一个值,这里就是在 int、double、char 中任选一个匹配的。
enum 枚举#
enumerator=枚举器 声明符号名称来表示整型变量。(enum常量本质就是int类型,一列出来默认从0开始赋值⬇️)
enum长度不确定会带来可移植性问题,如果第三方库API接口使用enum类型,编译和调用库时一旦有关enum长度的编译器设置不一致,API接口层对数值的解析就不匹配。
Caution
其实 enum 的大小是与编译器相关的,gcc有选项-fshort-enums,keil中可以通过设置short enum,iar中是默认是short
enum spectrum {red, orange, yellow, green, blue, violet}; // 1.枚举符号
enum spectrum color; // 2.
- 这里创建了
spectrum作为标记名,现在enum spectrum就是一个类型名了。 - 这里声明了类型为
enum spectrum的color变量
int c;
color = blue;
if (color == yellow)
// ...
for (color = red; color <= violet; color++) {
// ... // C++中不能对枚举变量使用自增运算 C中可以
}
可以这样使用,此处的 spectrum 枚举符号范围是0~5,所以编译器可以采用u char来表示color
enum 可以在任何 int 能用的地方用,比如 switch-case 中作为标签
当然,枚举声明中也可以手动赋值:
enum feline {cat, lynx = 10, puma, tiger};
cat 为0,lynx 10, puma 11, tiger 12
typedef 类型定义#
typedef unsigned char BYTE;
BYTE x, y[10], *z;
typedef char * STRING;
STRING name, sign; // == char * name, * sign;
有关指针,还可以如上所示⬆️
typedef struct complex {
float real;
float imag;
} COMPLEX;
可用于结构,为这个 struct complex 结构创建了一个别名叫做 COMPLEX
Important
typedef 并没有创建任何新类型,只给某个存在的复杂类型创建了一个方便使用的标签。
其他复杂声明#
int * risks[10]; // 指针数组,10个指向int的指针
int (* risks)[10]; // 数组指针,指向一个数组,数组包含10个int
Important
[] 和 () 有 相同 优先级,从左往右 结合,他们比 * 优先级高
位运算#
按位运算符的优先级比 == 要高,所以记得添加括号。
移位运算#
#define BYTE_MASK 0xff
unsigned long color = 0x002a162f;
unsigned char red, green, blue;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
red = (color >> 16) & BYTE_MASK;
使用一个unsigned long存储颜色数据,可以通过位移运算+掩码的方式将他取出来。嵌入式常用
位字段#
位字段是一个 signed int 或 unsigned int 类型变量中的一组相邻的位
struct {
unsigned int field1 : 1;
unsigned int : 2;
unsigned int field2 : 1;
unsigned int : 0;
unsigned int field3 : 1;
} stuff;
字段不允许跨越两个 unsigned int ,所以这里需要使用未命名的字段宽度填充。这里的 field3 将被存储在下一个 unsigned int 中。(可能大端也可能小端存储。)
Note
位字段并不常用,因为它涉及类型长度的定义,难以在不同机器间移植。
C预处理#
预处理的预处理#
- 先将代码字符集映射到源字符集,用于处理多字节字符
- 定位所有
反斜杠后带换行符的地方,将跨行代码合并成一行 - 划分预处理记号序列、空白序列、注释序列。所有空白序列(不含回车)换成一个空格,所有注释块都会换成一个空格字符。
- 准备进入预处理阶段找到第一行
#
#define 明示常量#
在文件中寻找宏的示实例,替换为替换体。单纯替换不进行运算。双引号中的宏不会被替换。
# 与 ## 运算符#
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 变成 int x1 = 14;
int XNAME(2) = 20; // 变成 int x2 = 20;
int x3 = 30;
PRINT_XN(1); // 变成 printf("x1 = %d\n", x1);
PRINT_XN(2); // 变成 printf("x2 = %d\n", x2);
PRINT_XN(3); // 变成 printf("x3 = %d\n", x3);
return 0;
}
# 用于将宏记号转化为字符串。
## 用于将两个记号拼在一起。 ⬆️上述例子中已经展示得非常清楚了。
#include 文件包含#
#include <stdio.h> // 系统路径
#include "mystuff.h" // 当前目录
#include "/usr/biff/p.h" // 指定目录
#undef 取消定义#
#define LIMIT 400
// ...
#undef LIMIT
移除之前的定义,现在 LIMIT 可用作其它新值。
条件编译#
优秀的条件编译可以让程序在各个平台上高效移植,只需修改头文件几个关键定义即可完成。
#ifdef #else #endif#
#ifdef MAVIS
#include "horse.h" // 如果已经用#define定义了 MAVIS,则执行下面的指令
#define STABLES 5
#else
#include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
#define STABLES 15
#endif
可以嵌套,新式编译器才支持缩进格式,否则需要左顶格。
#ifndef#
用法与 #ifdef 相同,也可以搭配 #else #endif 使用,语义与 #ifdef 相反,表示未定义某某
预定义宏#
// predef.c -- 预定义宏和预定义标识符
#include <stdio.h>
void why_me();
int main() {
printf("The file is %s.\n", __FILE__); // 当前文件名
printf("The date is %s.\n", __DATE__); // 预处理时间
printf("The time is %s.\n", __TIME__); // 预处理时间
printf("The version is %ld.\n", __STDC_VERSION__); // c版本
printf("This is line %d.\n", __LINE__); // 现在是第几行
printf("This function is %s\n", __func__); // 现在是什么函数
why_me();
return 0;
}
void why_me() {
printf("This function is %s\n", __func__);
printf("This is line %d.\n", __LINE__);
}
#pragma #line #error 与泛型选择#
感觉用不太到🤔
inline 内联函数#
将函数调用替换为函数体。加快函数调用速度。一般不用于大型函数,一般用来让需要多次调用的小函数调用加速。
库函数#
sprintf & snprintf#
标准函数原型如下:
int sprintf(char *str, const char *format, ...);
char *str(Destination Buffer - 目标缓冲区)- 指向字符数组的指针,
sprintf将会把最终生成的结果字符串写入这个数组。确保这个数组有足够大的空间来存放最终的字符串,包括字符串末尾的结束符\0
- 指向字符数组的指针,
const char *format(Format String - 格式化字符串)- 这是一个字符串,输出的模板
...(Ellipsis - 可变参数)- 这是一个可变参数列表。这里的参数就是你要格式化的实际数据(变量、常量等)
- 返回值 (Return Value)
- 成功时:返回成功写入到
str缓冲区的字符总数,不包括结尾的空字符\0。 - 失败时:返回一个负值。
- 成功时:返回成功写入到
C99中更安全的选择:snprintf
int snprintf(char *str, size_t size, const char *format, ...);
snprintf会保证写入的字符数(包括结尾的\0)绝不会超过size
