第一部分,我们先来复习一下C语言的基本语法,在这里,我不会覆盖到C语言的所有语法,而会列出一些C语言里容易引起困惑的知识点以及gcc编译器与其他编译器不同之处.完整的C语言语法,请参考C11国际标准以及GCC官方文档.
在gcc中,布尔类型的变量以_Bool
形式表示,且只能赋值为0
和1
.
在stdbool.h
文件中,以宏定义的形式定义了bool
,true
,false
等关键字.
#include
#include // 若注释掉此文件,会报错: unknown type name ‘bool’;‘true’,'false' undeclared
int main() {
bool b1 = true, b2 = false;
return 0;
}
sizeof
是一个特殊的运算符,用来计算某变量或数据类型在内存中所占的字节数.
#include
int main() {
int m = 10;
int a[] = {1, 2, 3, 4, 5};
int *p = a;
printf("%d\n", sizeof(int)); // int类型变量占4个字节
printf("%d\n", sizeof(m)); // 变量 m 占4个字节
printf("%d\n", sizeof(10)); // 变量 10 占4个字节
printf("%d\n", sizeof(a)); // 数组 a 占20个字节
printf("%d\n", sizeof(p)); // 指针 p 占8个字节(与处理器位数有关)
return 0;
}
声明变量时的完整语法是<存储类型> <变量类型> <变量名>
,其中<存储类型>
常常不写,默认为auto
.
变量的存储类型有如下四种:
关键字 | 存储类型 |
---|---|
auto (默认) |
默认型 |
static |
静态型 |
register |
寄存器型 |
extern |
外部型 |
auto
:默认型
auto
是默认的变量存储类型,说明变量只能在程序某个范围内(函数体内或函数的复合语句内)使用,其初值默认为随机值.
#include
int main() {
{
auto int a = 10;
}
printf("%d", a); // 报错: ‘a’ undeclared
return 0;
}
static
:静态型
static
类型的变量既可以在函数体内声明,也可以在函数体外声明.有以下两个性质:
static
类型的变量存储在静态存储区,而不是在堆栈中.static
类型变量不会被回收,一直保存着上一次调用存入的值.#include
int main() {
for (int i=0; i<5; i++) {
static int a = 5;
a++;
printf("%d ", a);
}
return 0;
}
运行上述程序,得到6 7 8 9 10
register
:寄存器型
register
关键字建议编译器将该变量放入寄存器中,以加快程序的运行速度.若申请不到寄存器,则该变量退化为auto
型.
#include
int main() {
register int a = 10;
printf("a=%d %p", a, &a); // 报错: address of register variable ‘a’ requested
return 0;
}
extern
:外部型
extern
用于说明该变量应用了外部作用域内声明的变量.下面例子演示其使用:
创建文件global_1.c
如下:
int global_a = 10;
创建文件global_2.c
如下:
#include
extern int global_a;
int main() {
printf("global a = %d\n", global_a);
return 0;
}
将两个文件一起编译:
gcc global_1.c global_2.c
程序输出global a = 10
.
static
修饰的变量,其他文件无法引用.
使用scanf()
读取字符时,有可能读到缓冲区内残余的一些垃圾.
#include
int main() {
int x;
char ch;
scanf("%d", &x);
scanf("%c", &ch);
printf("x=%d, ch=%c\n", x, ch);
return 0;
}
输入10↵c
后,程序输出x=10, ch=↵
.这是因为scanf
把我们用作分隔符的回车↵赋给了ch
.
有两种方式解决这种问题:
使用getchar()
函数清除缓冲区内容:
#include
int main() {
int x;
char ch;
scanf("%d", &x);
getchar(); // 清空缓冲区
scanf("%c", &ch);
printf("x=%d, ch=%c\n", x, ch);
return 0;
}
使用模式匹配字符%*c
或匹配缓冲区内容:
#include
int main() {
int x;
char ch;
scanf("%d", &x);
scanf("%*c%c", &ch); // 或scanf(" %c", &ch);
printf("x=%d, ch=%c\n", x, ch);
return 0;
}
优先级 | 运算符 | 结合规律 |
---|---|---|
1 | [] ,() ,. ,-> ,后缀++ ,后缀-- |
从左向右 |
2 | 前缀++ ,前缀-- ,sizeof ,& ,^ ,+ ,- ,! |
从右向左 |
3 | 强制类型转换 | 从右向左 |
4 | ^ ,/ ,% (算数乘除) |
从左向右 |
5 | + ,- (算术加减) |
从左向右 |
6 | << ,>> (位移位) |
从左向右 |
7 | < ,<= ,> ,>= |
从左向右 |
8 | == ,!= |
从左向右 |
9 | & (位逻辑与) |
从左向右 |
10 | ^ (位逻辑异或) |
从左向右 |
11 | | (位逻辑或) |
从左向右 |
12 | && |
从左向右 |
13 | || |
从左向右 |
14 | ? : |
从右向左 |
15 | = ,^= ,/= ,%= ,+= ,-= ,<<= ,>>= ,&= ,^= ,|= |
从右向左 |
16 | , |
从左向右 |
指针与一维数组
一维数组的数组名即为数组的指针,指向数组的起始地址.设指针px
指向数组x[]
,则下面四种写法具有完全相同的意义:
x[i]
,*(px+i)
,px[i]
,*(x+i)
.
#include
int main() {
int x[] = {3, 6, 9};
int *px=x, n=sizeof(x) / sizeof(int);
printf("%p %p %p\n", x, x+1, x+2); // 输出 0x7ffc45767d8c 0x7ffc45767d90 0x7ffc45767d94
for (int i=0; i<n; i++) {
printf("%d %d %d %d\n", x[i], *(px+i), px[i], *(x+i)); // 输出 3 3 3 3↵6 6 6 6↵9 9 9 9
}
return 0;
}
指针与二维数组
二维数组名为数组的行指针,指向数组的起始地址.数组名加1表示移动一行元素.设行指针px
指向数组x[][]
,则下面四种写法具有完全相同的意义:
x[i][j]
,*(*(px+i)+j)
,px[i][j]
,*(*(x+i)+j)
.
行指针不同于二重指针,行指针用于存储行地址,其定义方式为<存储类型> <数据类型> (*<行指针变量名>)[每行元素数]
#include
int main() {
int x[][2] = {{1, 6}, {9, 12}, {61, 12}};
int (*px)[2]; // 定义一行指针px,每行2个元素
px = x;
printf("%p %p\n", x, x+1); // 输出 0x7ffc539cf720 0x7ffc539cf728
printf("%p %p\n", px, px+1); // 输出 0x7ffc539cf720 0x7ffc539cf728
printf("%d %d %d %d\n", x[1][1], *(*(px+1)+1), px[1][1], *(*(x+1)+1)); // 输出 12 12 12 12
return 0;
}
行指针和二重指针的一个区别在于: 执行
++
操作时,行指针移动一行数据所占字节,而二重指针只移动一个指针变量所占字节.
字符指针与字符串
在C语言中,字符串常量被存放在静态存储区内.字符串指针存储的是字符串的首地址,而不是其中的内容.
#include
#include
int main() {
char *p1 = "hello world";
char *p2 = "hello world";
printf("&p1=%p, p1=%p, %s\n", &p1, p1, p1);
printf("&p2=%p, p2=%p, %s\n", &p2, p2, p2); // 两指针指向同一块内存空间
return 0;
}
运行上述程序,得到:
&p1=0x7ffd013dc848, p1=0x559d63a357c4, hello world
&p2=0x7ffd013dc850, p2=0x559d63a357c4, hello world
在C语言中,有三类变量会被存入静态存储区: 全局变量,
static
修饰的变量,字符串常量.
不能通过字符串指针索引修改字符串,但可以通过数组名索引修改字符串数组.
#include
#include
int main() {
char array[] = "hello world"; // 字符串数组
char *pointer = "hello world"; // 字符串常量
// 字符串数组和字符串常量分别存储在内存的不同区域
printf("&array=%p, array=%p, %s\n", &array, array, array);
printf("&pointer=%p, pointer=%p, %s\n", &pointer, pointer, pointer);
// 可以通过数组名索引修改该字符串数组的内容
array[3] = 'o'; // 或 *(array+3)='o';
printf("&array=%p, array=%p, %s\n", &array, array, array);
// 不可以通过指针名索引修改字符串常量中的内容
pointer[3] = 'o'; // 或 *(pointer+3)='o';
printf("&pointer=%p, pointer=%p, %s\n", &pointer, pointer, pointer); // 发生Segmentation fault 错误
return 0;
}
可以看到,尽管内容相同,array
和pointer
变量被保存在内存的不同地址.且当执行到pointer[3] = 'o';
语句时,发生Segmentation fault
错误.
&array=0x7ffe8e5639ac array=0x7ffe8e5639ac hello world
&pointer=0x7ffe8e5639a0 pointer=0x55ec90415814 hello world
&array=0x7ffe8e5639ac array=0x7ffe8e5639ac heloo world
Segmentation fault (core dumped)
常量指针的声明方式为:<数据类型> * const <指针变量名称>
.
常量指针所指向的变量被视为一个常量,不可通过指针修改其值.但指针本身可以修改.
指针常量的声明方式为:const <数据类型> * <指针变量名称>
.
指针常量本身不能修改.但我们可以通过* <指针名>
修改所指向的变量的值.
const
关键字修饰谁,谁就是常量.
#include
int main() {
int n = 10;
const int * p1 = &n; // 常量指针,不能通过p1修改n的值,但p1本身可以修改
int * const p2 = &n; // 指针常量,可以通过p2修改n的值,但p2本身不能修改
// *p1 = 20; // 报错: assignment of read-only location ‘*p1’
p1 = NULL; // 可以修改p1本身
*p2 = 20; // 可以通过p2修改其所指向的变量n
// p2 = NULL; // 报错: assignment of read-only variable ‘p2’
return 0;
}
在p1
的定义语句中,const
修饰的是int * p1
整体,因此将所指向的量(location)视为常量;在p2
的定义语句中,const
修饰的是p2
本身,因此将指针变量本身(variable)视为常量.
main()
函数参数的意义在C语言中,main()
函数完整的函数原型是int main(const int argc, const char * argv[])
.
其中argc
代表传入程序的参数个数+1;argv[0]
指向函数名字符串,argv
的其他元素依次指向传入程序的各个参数.
以下面程序为例,创建文件show_args.c
,内容如下:
#include
int main(const int argc, const char * argv[]) {
printf("argc=%d\n", argc);
for (int i=0; i<argc; i++) {
printf("%s\n", argv[i]);
}
return 0;
}
使用gcc编译该程序:gcc show_args.c -o show_args
,生成可执行文件show_args
.
调用该可执行文件并传入参数:./show_args 192.168.1.7 6000
,程序输出如下:
argc=3
./showargs
192.168.1.7
6000
使用数组作为函数参数时,形参可以写成指针的形式(void fun(int* array)
),也可以写成数组名的形式(void fun(int array[])
).但不管哪种书写形式,形参都会被视为指针而非数组.
下面程序中的array_sum()
函数试图对数组进行求和,但是算出了错误的结果,因为函数的形参array
被视为指针而非数组,对其调用sizeof
运算符得到的是数组指针所占字节数,而非数组所占字节数.
#include
int array_sum(int array[]); // 等价于 int array_sum(int * array); 本质上是一样的
int main() {
int a[] = {1, 2, 3, 4, 5};
int sum = 0;
printf("实参数组地址: %p", a);
sum = array_sum(a);
return 0;
}
int array_sum(int array[]) {
printf("形参数组地址: %p", array);
int sum = 0;
int n = sizeof(array)/sizeof(int); // 在这里,array被视为指针而非数组名,造成bug
for (int i=0; i<n; i++) {
sum += array[i];
}
return sum;
}
要在函数中对传入数组进行遍历,一定要向其传入数组的长度,上面程序可以这样改写:
int array_sum(int array[], int n) {
printf("形参数组地址: %p", array);
int sum = 0;
// int n = sizeof(array)/sizeof(int); // 在这里,array被视为指针而非数组名,造成bug
for (int i=0; i<n; i++) {
sum += array[i];
}
return sum;
}
函数指针用于存放函数的入口地址,可以通过函数指针对调用函数,有点类似于其他高级语言中的匿名函数.声明函数指针的语法如下:
<数据类型> (*<函数指针名称>)(<参数列表>)
下面例子演示函数指针的使用:
#include
int add(int a, int b) {
return a+b;
}
int sub(int a, int b) {
return a-b;
}
int multiply(int a, int b) {
return a*b;
}
int main() {
int(*p)(int, int); // 定义一个函数指针p,对应的参数列表为(int, int),返回值为int
p = add;
printf("%d\n", (*p)(30, 20)); // 输出 50
p = sub;
printf("%d\n", (*p)(30, 20)); // 输出 10
p = multiply;
printf("%d\n", (*p)(30, 20)); // 输出 600
}
函数指针广泛应用于各种库函数中,例如下面几个函数:
排序函数qsort()
需要一个函数指针参数指定数据的比较方法:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
线程创建函数pthread_create()
需要一个函数指针参数指定线程执行的任务:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
C语言的条件编译可以根据宏定义编译代码,这是在预处理阶段进行的.
条件编译指令 | 说明 |
---|---|
#ifdef |
如果该宏已定义,则执行相应操作 |
#ifndef |
如果该宏没有定义,则执行相应操作 |
#if |
如果条件为真,则执行相应操作 |
#else |
如果前面条件均为假,则执行相应操作 |
#elif |
如果前面条件为假,而该条件为真,则执行相应操作 |
#endif |
结束前面对应的条件编译指令 |
下面两个程序展示了条件编译的使用
#include
#define _DEBUG_ // 定义宏变量_DEBUG_
int main() {
# ifdef _DEBUG_
printf("The macro _DEBUG_ is defined.\n");
# else
printf("The macro _DEBUG_ is not defined.\n");
# endif
return 0;
}
#include
#define _DEBUG_ 1 // 定义宏变量_DEBUG_并赋值为1
int main (void)
{
#if !_DEBUG_
printf("The macro _DEBUG_ is 0.\n");
#else
printf("The macro _DEBUG_ is 1.\n");
#endif
return 0;
}
结构体变量的定义有3种方法
先定义结构体类型再定义变量
struct 结构体名 {
成员列表;
};
struct 结构体名 变量名1, 变量名2;
在定义结构体的同时定义变量
struct 结构体名 {
成员列表;
} 变量名1, 变量名2;
不定义结构体类型名,直接定义结构体变量
struct {
成员列表;
} 变量名1, 变量名2;
这种方式常用于定义结构体内部嵌套的结构体,类似于其他高级语言的匿名类.
struct student {
int id;
char name[20];
struct {
int year;
int month;
int day;
} birthday;
};
struct student stu1 = {01, "zhangsan", {1998, 1, 1}};
struct student stu2 = {02, "lisi", {1999, 1, 1}};
需要注意的是,结构体变量的变量类型为struct 结构体名
,单独使用结构体名
声明变量时不被允许的,即使在VC等编译器中可以这样做.
struct student {
int id;
char name[20];
};
// student stu1; // 报错: unknown type name ‘student’; use ‘struct’ keyword to refer to the type
struct student stu2;
可以使用typedef
给结构体类型取别名.在下面的例子中,我们定义了两个新的数据类型listnode
和linklist
,它们分别等价于struct _node_
和struct _node_ *
.
tyoedef struct _node_ {
int data;
struct _node_ *next;
} listnode, *linkist;
使用union
关键字定义共用体,定义共用体变量的语法和结构体类似,他们的本质区别仅在于内存的使用方式上.共用体中的变量成员占据同一处内存空间,为共用体的一个成员变量赋值后,其它成员变量就失去了作用.
#include
struct struct_example {
int i;
char c;
double d;
};
union union_example {
int i;
char c;
double d;
};
int main() {
printf("size of struct struct_example = %d\n", sizeof(struct struct_example)); // 输出16
printf("size of union union_example = %d\n", sizeof(union union_example)); // 输出8
}
C/C++语言中定义了四个内存区间:
局部变量存储在栈区,而动态内存分配发生在堆区.
动态内存分配发生在堆区,在分配堆区数据时不会做初始化操作(即数据初值不是0),必须显示初始化.
使用malloc()
和free()
函数进行堆区内存的申请和释放,两个函数的原型如下:
void* malloc(size_t size);
void free(void *ptr);
malloc(size_t size)
函数本身并不关心所申请的内存是什么类型,只关心申请内存的总字节数size
,返回一个void *
指针,需要我们将其显式转换成我们所需要的类型.
malloc()
函数申请到的是一片连续的内存空间,有时会申请不到符合大小的内存,返回NULL
,因此我们需要对malloc()
函数的返回值进行判断后再进行下一步操作.
free(void *ptr)
函数的作用是删除指针ptr
指向的变量,回收其内存空间,而不是删除ptr
指针本身,调用free()
函数过后,ptr
变为了野指针,为了安全着想,应尽快将其设为NULL
.
内存泄漏: 若malloc()
函数返回的指针值丢失,我们就无法访问该内存区域,进而无法回收该内存区域.这被称为内存泄漏现象.为避免内存泄漏现象的产生,我们应尽量保证malloc()
与free()
成对出现在同一函数体内.
使用动态内存分配可以使得变量的定义更加灵活.下面的例子中,需要将get_string()
函数中的字符串变量s
传回主函数,使用其它方式定义变量都会存在各种问题,只能使用动态内存分配的方式.
#include
#include
#include
char * get_string() {
// char s[] = "welcome"; // 数组s是局部变量,无法将内容传递到函数外
// char * s = "welcome"; // 指针s指向字符串常量,无法修改其内容
// static char s[] = "welcome"; // 数组s被存储在静态存储区,程序结束之前无法回收该块内存
char * s = (char *)malloc(10 * sizeof(char));
if (s == NULL) { // 对malloc()函数的返回值应先判断再使用
printf("malloc failed\n");
return NULL;
}
strcpy(s, "welcome");
return s;
}
int main() {
char * p;
p = get_string();
printf("%s\n", p);
free(p);
p = NULL; // 释放内存后应尽快将对应指针设为NULL;
return 0;
}