C

C语言使用预处理器创建常量, 相关数值在编译时会直接嵌入代码的相应位置.
#define ROWS 3
#define COLS 4*
这是一个在代码规范良好时不需要注意的语言特征, 在大多数情况下可以忽视.
序列点指的是表达式"副作用"发生的位置.
C语言的分号就是一个序列点, 代表一条语句的结束.
拿赋值语句举例, 在程序运行到序列点时, 赋值确保已经完成.
完整表达式指一条完整的表达式, 也就是说这条表达式不是另一条表达式的子表达式.
任何一条完整表达式都是一个序列点.
考虑以下语句:
y = (4 + x++) + (6 + x++);
此语句的整条赋值表达式是完整表达式.
由于 4+ x++ 不是完整表达式, 不是序列点, 所以C语言不保证该表达式在求值后会让变量x递增.
此外, C语言中未定义应该先对前一条子表达式求值再递增, 还是对所有表达式求值之后再递增,
所以这条赋值表达式中存在着危险的未定义行为, 应该避免编写类似的代码.
逗号运算符是一个序列点, 逗号运算符最常见的使用场景是在for循环的头部中分隔多个表达式.
逗号运算符从左向右运行用逗号分隔的表达式(每个表达式都有一个序列点), 其值为最后一个表达式的结果.
旧版本的C语言并不存在布尔类型, 逻辑运算由数字代劳.
C99新增了 _Bool 类型(由于向前兼容的需要, C语言的新增类型通常都使用这种命名方式).
通过引入stdbool.h头文件, 可让bool成为_Bool的别名.
C语言中的const是一种访问修饰符, 而不是常量.
带有const修饰符的形参不可更改, 通常用于数组等指针类型, 用于表示该函数不产生修改相关值的副作用.
int sum(const int ar[], int n); // 函数原型
int sum(const int ar[], int n) {
...
}
int * pi;
char * pc;
float * pf, * pg;
// 声明指针时留有空格是一种约定成俗的规范
被const修饰的指针不得修改其指向的值, 但一个const指针可以重新赋值以指向其它地址.
一个没有const修饰的指针不得指向一个被const修饰的指针(否则该限制就形同虚设了).
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates;
*pd = 29.89; // 不允许
pd[2] = 222.22; // 不允许
rates[0] = 99.99; // 允许
当const同时修饰于指针和指针地址代表的值时, 即是双重const指针.
双重 const 指针连重新赋值以指向其他地址这一行为也被禁止了.
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * const pc = rates;
pc = &rates[2]; //不允许
*pc = 92.99; //不允许
取引用运算符 & 后跟一个变量名时, 取出该变量的内存地址.
解引用运算符 * 后跟一个指针名或地址时, 取出该地址代表的值.
nurse = 22;
ptr = &nurse;
val = *ptr;
dates + 2 == &date[2]
*(dates + 2) == dates[2]
指针加法是: (当前的地址) + (指针指向的数据类型所占的内存) * (增加值)
指针减法是指针加法的逆运算.
举例来说, 如果指针是数组中一个元素的地址,
那么 指针+1 的结果就是该元素相邻的下一个元素的地址, 指针-1 的结果就是该元素相邻的上一个元素的地址.
当数学加法和减法运算的左值右值都是指针时, 可以求出两个指针之间的内存距离(以指针指向的数据类型表示).
只建议对同一个数组内的两个指针求差.
空指针是指向NULL的指针, 意味着它不指向任何有意义的地址.
在内存中, NULL值以0表示.
悬空指针是不指向任何合法对象的指针.
会意外产生悬空指针的典型原因是指向地址里的对象已经被回收了, 但相关的指针并没有更新为NULL.
C语言的数组本质上是一种带有类型内存大小信息的指针.
数组名本身就是第一个元素的地址, 取数组下标索引只是语法糖.
int arr[6] = {0,0,0,0,0,212};
int arr[6] = {[5] = 212}; // C99, 未初始化的值会初始化为0
int arr[6] = {[4] = 31,30}; // 同时初始化索引为4, 5的元素, 后初始化的结果将覆盖之前的
int arr[] = {1, [6] = 23}; // 由编译器设置数组为可以容纳得下的固定大小
C语言未定义数组下标越界时发生的行为.
C语言数组下标越界时, 通常会根据数组的数据类型所占的内存大小访问到位于数组后方的内存地址,
也就是访问了预期以外的内存地址.
C语言如此设计是因为在允许下标越界的情况下, 编译器无需在每次数组访问时都执行一次是否越界的检查,
这有利于C语言代码达到理论上的最高性能.
绝大多数的C语言代码的编译结果都与等价的手写汇编代码相同, 可见C语言是非常底层的高级语言.
由于C语言的数组类型只是指针, 所以当把数组传给函数时, 还需要手动将数组的大小作为参数传递.
以下四种函数原型等价:
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);
以下两种函数定义等价:
int sum(int *ar, int n) { ... }
int sum(int ar[], int n) { ... }
将多维数组作为函数形参时必须指定第一维以外的数组的大小.
int sum2d(int ar[][4], int rows);
int sum2d(int (*ar)[4], int rows);
C99新增了变长数组(VLA)作为语言特性, 变长数组是一种自动存储类别.
变长数组是一种可以在运行时动态指定数组大小的数组, 在C11支持VLA作为可选特性以前,
想要创建变长数组只能通过手动申请内存空间的方法, VLA简化了这一工作.
变长数组在声明以后跟一般数组一样, 其大小不再能够改变.
int quarters = 4;
int regions = 5;
double sales[regions][quaters];
由于变长数组需要维度信息, 必须在声明该形参之前, 先声明其需要的其他形参.
例子(先声明了rows和cols, 然后声明变长数组):
int sum2d(int rows, int cols, int ar[rows][cols]);
C语言的字符串本质上是字符数组.
连续的字符串字面量会被自动联接起来.
char greeting[] = "Hello, and"
" how are"
" you"
" today!";
char greeting[] = "Hello, and how are you today!";
当要表示字符串数组时, 一般采用指针形式而不是二维字符数组.
这是因为指针指向的字符串在编译时被储存在静态内存中(不可修改),
而二维字符数组保存的是由单个字符组成的二维数组,
后者的单个字符数组尺寸被限制为整个字符串数组中最长字符串的长度, 导致多余的内存空间被浪费.
const char *mytalents[5] = {
"Adding numbers swiftly",
"Multiplying accurately", "Stashing data",
"Following instructions to the letter",
"Understanding the C language"
}
  • extern 外部链接, 用于引入其他文件里的变量.
  • static 静态存储类别, 作用域受限于文件.
像extern这样的存储类别之所以存在, 是因为计算机底层实现库的链接靠的是内存地址的链接.
因此, 如果不特别标明外部引用和外部定义这种元数据, 链接器就找不到对应的内存地址.
这也是为什么C语言的程序可以先编译, 后链接.
在计算机性能很差的年代, 链接技术被用来避免重复编译未改动的代码,
以缩短编译时间, 后来才成为一种划分软件模块的技术.
使用include在编译时引入其他文件.
include有两种形式, 分别使用双引号文件名和尖括号文件名.
双引号文件名指示编译器在本地查找文件.
尖括号文件名指示编译器在标准头文件处查找文件, 也即编译器内建的库.
C11新增的嵌套匿名成员结构:
struct person {
int id;
struct {
char first[20];
char last[20];
}
}
嵌套的成员可像一般成员一样访问:
struct person ted = {8483, {"Ted", "Grass"}};
ted.first;
该功能主要服务于联合(union)类型:
struct owner {
char socseurity[12];
};
struct leasecompany {
char name[40];
char headquarters[40];
};
struct car_data {
char make[15];
int status;
union {
struct owner owncar;
struct leasecompany leasecar;
};
};
struct book suprise = { .value = 10.99 };
需要左值是一个结构指针, 这是最常用的方法.
ptr->member
需要左值是一个结构, 由于点运算符的优先级比较高, 所以需要括号完成解引用.
(*ptr).member
枚举值为常量, 默认情况下枚举值从0开始自增.
enum {low =100, medium = 500, high = 2000};
enum feline {cat, lynx = 10, puma, tiger}; // cat = 0, puma = 11, tiger = 12
int board[8][8]; // 声明一个内含int数组的数组
int ** ptr; // 声明一个指向指针的指针,被指向的指针指向int
int * risks[10]; // 声明一个内含10个元素的数组,每个元素都是一个指向int的指针
int (* rusks)[10]; // 声明一个指向数组的指针,该数组内含10个int类型的值
int * oof[3][4]; // 声明一个3×4 的二维数组,每个元素都是指向int的指针
int (* uuf)[3][4]; // 声明一个指向3×4二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]; // 声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
char * fump(int); // 返回字符指针的函数
char (* frump)(int); // 指向函数的指针,该函数的返回类型为char
char (* flump[3])(int); // 内含3个指针的数组,每个指针都指向返回类型为char的函数