C 语言笔记
操作符
-
‘- -’ ,‘++’
后置:先使用再操作
前置:先使用后赋值 -
(类型符) 强制类型转化
-
条件操作符 exp1 ? exp2 : exp3
exp1 为真, exp2 执行,为假 exp3 执行。
-
下标引用
[ ] ----------- (数组)下标引用操作符
( ) ----------- 函数调用操作符
. ------------- 结构体.成员
-> ----------- 指针-> 成员
常见关键字
- auto
自动变量 == 局部变量,一般省略。
auto int a = 10;
- register
寄存器变量,高速访问。 - typedef
类型定义–重定义 - extern
在调用其他.c文件中的变量或者函数时,需要在使用前用extern申明。 - static
当修饰局部变量时,局部变量的生命周期变长,下次进入局部作用域时变量值保留(跳过变量初始化赋值);
当修饰全局变量时,改变作用域。静态的全局变量只能在自己所在的源文件内部使用(extern 使用无效);
当修饰函数时,改变函数的链接属性,外部链接变为内部链接属性(extern 使用无效)。 - #define
可定义标识符常量;
#define PI 3.1415926
可定义函数(宏);
#define MAX(X,Y) (X>Y?X:Y)
- const
const 修饰的变量无法修改;但是可以通过指针修改。
const int num = 10;
int* p = #
*p = 20;
const 修饰指针时有两种情况。
const int num = 10;
const int* p = #
// 错误!!!!
*p = 20; // const 放在指针变量的*左边时,修饰*p,不能改变*p的值
const int num = 10;
int num_2 = 20;
int* const p = #
// 错误!!!!
p = &num_2; // const 放在指针变量的*右边时,修饰p,不能改变p的值
但是修饰指针时情况不一样。
数组
数组名
- 数组名是数组首元素的地址。(有两个例外)
1.sizeof(数组名),计算整个数组的大小。
2.&数组名,取出的是数组的地址。
指针
指针数组和数组指针
int* p[10];
int(*p)[10];
[ ] 比*优先级更高。第一个是数组,存放10个指向int的指针;第二个是指针,指向含有10个int型的数组。
函数指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = &Add;
int (*p)(int, int) = Add;
return 0;
}
*p 取的是函数 Add 的地址,() 表示函数。
void(*signal(int, void(*)(int)) )(int);
typedef void(*fun)(int);
fun signal(int, fun);
signal是一个函数申明;
signal函数参数有两个,第一个是int,第二个是函数指针,该函数指向的函数参数是int,返回类型是void;
signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回值是void。
空指针
void * p;
该指针可以接受任意类型的地址;但是不能进行解引用和±
结构体
结构体内存对齐
- 第一个成员放在首地址处;
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
vs 中默认的值为8 - 结构体的大小为最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐原因
6. 平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出异常。
7. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说,内存对齐是以空间换时间。
设置默认对齐数
#include <stdio.h>
#pragma pack(4) //设置默认对齐数为4
struct S
{
char c1;
double d1;
};
#pragma pack() //取消默认对齐数
位段
声明
- 位段成员必须是整型量。
- 位段成员名后面有一个冒号和数字。
- 冒号后的数字代表所占比特数(二进制位)。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
};
内存分配
- 位段的成员可以是整型量或者char(属于整型家族)。
- 位段的空间上是按照需要以4个字节或者一个字节的方式来开辟的。
- 位段涉及很多不确定因素,是不跨平台的,可移植程序应避免使用位段。
枚举
把可能的值一一列举,只能用里面的值进行结构体赋值。
enum Sex
{
MALE, // 默认0,可以更改 MALE = 2,
FEMALE, // 默认1
SECRET, // 默认2
};
优点
- 增加代码的可读性和可维护性
- 和 #define 定义的标识符比较,枚举有类型检查,更加严谨
- 防止命名污染,便于调试
- 使用方便,一次可以定义多个常量
联合体(共用体)
定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间。
union un
{
char c;
int i;
};
特点
联合体的大小,至少是最大成员的大小。
联合体的成员不能同时修改。
动态内存管理
malloc 和 free
- void* malloc ( size_t size);
- void free (void* memblock);
#include <stdlib.h>
int* p = (int*)malloc(10 * sizeof(int));
free(p); //p指针的地址没改变,依旧可以修改内容。
p = NULL; // 改变地址。
calloc
- void* calloc(size_t num, size_t size);
分配空间并置零。
int* cp = (int*)calloc(10, sizeof(int)); // 开辟10个整型空间并置零
free(p);
p = NULL;
realloc
- void* realloc(void* ptr, size_t size);
调整动态开辟空间的大小
int* p = (int*)malloc(10 * sizeof(int));
int* p1 = (int*)realloc(p, 20 * sizeof(int)); // 调整后新大小为20*4个字节
free(p1);
p1 = NULL;
p 和 p1指向的空间可能不一样。如果内存可以追加,则一样;若不能追加会重新开辟空间并将原数据拷贝到新空间,释放旧的内存空间,两者则不一样。
常见动态内存错误
- 对空指针解引用操作。malloc开辟失败,返回指针指向NULL。
- 对动态开辟内存的越界访问。程序卡死。
- 对非动态开辟内存的free。程序卡死。
int a = 10;
int* p = &a;
free(p);
- 使用free释放动态开辟内存的一部分。free释放要从起始位置开始。
int* p = (int*)calloc(10, sizeof(int));
p++;
free(p);
- 对同一块开辟的内存多次释放。可通过free后置空指针避免重复释放出错。
- 动态开辟内存忘记释放(内存泄露)。
柔性数组
结构中最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
struct st
{
int a;
int a_1[];
};
printf("%d\n", sizeof(struct st)); // 4个字节
struct st* s = (struct st*)malloc(sizeof(struct st) + 10 * sizeof(int)); // 为a_1开辟了10个整型变量空间。
与结构体内有指针成员,指针动态开辟空间相比,有以下好处:
- 方便内存释放(仅需一次);
- 有利于访问速度,减少内存碎片。
内存分配
- 栈区(stack):存储函数的局部变量(包括main函数),函数执行结束后自动释放。
- 堆区(heap):存储动态内存。一般由程序员分配释放,若不释放,程序结束后收回。
- 数据段(静态区 static):存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
文件操作
文件指针
每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的,该结构体类型是有系统声明的,取名 FILE。一般都是通过一个FILE 指针来维护这个FILE结构的变量,使用更方便。
文件的打开和关闭
- FILE* fopen ( const char* filename, const char* mode);
打开方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r” (只读) | 输入数据,打开已存在文本文件 | 出错 |
“w” (只写) | 输出数据,打开一个文本文件 | 创建新文件 |
“a” (追加) | 向文本文件尾添加数据 | 出错 |
“rb” (只读) | 输入数据,打开二进制文件 | 出错 |
“wb” (只写) | 输出数据,打开二进制文件 | 创建新文件 |
“ab” (追加) | 向二进制文件尾添加数据 | 出错 |
“r+” (读写) | 读写,打开文本文件 | 出错 |
“w+” (读写) | 读写,建一个文本文件 | 创建新文件 |
“rb+” (读写) | 读写,打开二进制文件 | 出错 |
“wb+” (读写) | 读写,建一个二进制文件 | 创建新文件 |
如果返回空指针,则打开失败。
- fclose ( FILE* stream);
文件的顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
文件随机读取
-
int fseek ( FILE * stream, long int offset, int origin );
从当前位置开始,从偏移量 offset 开始读取。origin可以有:
-
long int ftell ( FILE * stream );
返回值是偏移量。 -
void rewind ( FILE * stream );
将文件指针置于起始位置。 -
void perror ( const char * str );
打印str和错误信息(比strerror(errno)方便)。 -
int feof ( FILE * stream );
判断是否到文件末尾。若到末尾返回非0值;若没到返回0。 -
int ferror ( FILE * stream );
判断是否读取错误。
程序的环境和预处理
程序的翻译环境和执行环境
翻译环境
在这个环境中源代码被转换为可执行的机器指令(二进制指令)。包括编译和链接。
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所使用到的函数,而且它可以搜索程序员的个人程序库,将其需要的函数也链接到程序中。
编译可细分为:预处理,编译和汇编。
- 预处理
1.完成头文件包含,#include。
2.使用空格替换注释。
3.完成预处理指令,#define。
总的来说,对源码进行文本操作,生成 .i 文件。
- 编译
把 C 代码翻译成汇编代码(语法分析,词法分析,语义分析,符号汇总)。
对 .i 文件编译,生成 .s 文件。
- 汇编
把汇编代码,转化成二进制指令。
对.s 文件进行汇编,生成.obj 文件。
链接
- 合并段表。
- 符号表的合并和重定位。
将.obj 文件链接到一起,生成.exe 文件。
运行环境
- 程序必须载入内存中。在有操作系统的环境中,一般由这个操作系统完成。
- 程序的执行开始。调用main 函数。
- 开始执行程序。使用运行堆栈(stack),和静态内存(static)来存储变量。
- 终止程序。正常终止main 函数,也有可能意外终止。
预处理详解
预定义符号
符号 | 作用 |
---|---|
__ FILE __ | 代码目前所在的绝对路径 |
__ LINE __ | 代码目前所在的行号 |
__ DATA __ | 当前日期 |
__ TIME __ | 当前时间 |
__ FUNCTION __ | 当前函数名 |
#define 定义标识符
#开头的都是预处理指令:#include,#program,#if …。在标识符处有介绍。
# — 将参数名插入到字符串中
#define PRINT(X) printf("the value of "#X" is %d\n",X)
调用PRINT 时,#X处会输出传入的变量名。
## — 将两边的符合合成一个符号
#define CAT(X,Y) X##Y
int today1 = 2019;
printf("%d\n", CAT(today, 1)); \\ 输出2019
宏参数可以不限数据类型
#define MAX(X,Y) X>Y? X:Y
调用时可以传入整型量或者浮点型量。
宏与函数相比较
Pros:
1.在执行简单运算上,宏比函数在程序的规模和速度上更胜一筹。
2.宏是类型无关的,可适用于多种类型数据。
Cons:
1.使用宏时会直接替换,若宏很长,会大幅度增加代码长度。
2.宏无法调试。
3.宏由于类型无关,也不够严谨。
4.宏可能会带来运算符优先级的问题,导致程序出现错误。
#undef — 移除一个宏定义
#define CAT(X,Y) X##Y
#undef CAT
条件编译
在编译程序时,可以选择将一组语句有条件的编译。
常见的条件编译指令:
-
直接条件编译
if 常量表达式
// …
#endif -
多分支的条件编译
#if 常量表达式
// …
#elif 常量表达式
// …
#else
// …
#endif -
判断是否被定义
#ifdef DEBUG
// …
#endif#ifndef DEBUG
// …
#endif
文件包含
#include 指令可以使另外一个文件被编译。替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。如果一个源文件被包含10次,那就实际被编译10次。
-
本地文件包含
#include “file.h”。查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误。 -
库文件包含
#include <stdio.h>。查找头文件直接去标准路径下查找,如果找不到就提示编译错误。可以用“ ”的形式包含库文件,不过查找效率低,且不容易区分两者区别。 -
嵌套包含
如果一个文件被包含多次,会造成编译多次。一下方式避免:- #ifndef __TEST_H __
#define __TEST_H __
/ / …
#endif - #pragma once
- #ifndef __TEST_H __