跳过正文

重逢C语言

·4137 字

复习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

一个联合中只能存储一个值,这里就是在 intdoublechar 中任选一个匹配的。

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.
  1. 这里创建了 spectrum 作为标记名,现在 enum spectrum 就是一个类型名了。
  2. 这里声明了类型为 enum spectrumcolor 变量
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 intunsigned 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预处理
#

预处理的预处理
#

  1. 先将代码字符集映射到源字符集,用于处理多字节字符
  2. 定位所有反斜杠后带换行符的地方,将跨行代码合并成一行
  3. 划分预处理记号序列、空白序列、注释序列。所有空白序列(不含回车)换成一个空格,所有注释块都会换成一个空格字符。
  4. 准备进入预处理阶段找到第一行 #

#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
Haley
作者
Haley