C语言整理(待更新)
Note:根据 CSDN C语言技能树 整理的题目;初衷是因为C语言相对于Java,Python更加简洁,没有那么多花里胡哨的语言特性,用来练习算法题再合适不过了。
文章目录
一、C语言的发展
二、数据类型
- 变量类型, 生命周期 和 作用域
- 常量2种类型(数值和字符) 和 定义(#define和const)
Note:常量也可以是函数表达式,比如#define MAX(a,b) ((a > b) ? a : b)
,参考关系运算符(判断区间是否重叠) - 数据类型分类(原生,派生,用户自定义)
- 结构体内存对齐1,结构体内存对齐2,可参考c语言数据类型字节大小 和 精度,结构体内存对齐(如何计算结构体的大小),内存管理:固定分区与可变分区,操作系统: 动态(可变)分区分配,内存分配 & C++类对象内存对齐 -------保姆级解释
Note:
- C语言没有bool,string类型;基本类型包括字符型,浮点型,整型 和 空类型;派生类型包括数组,指针和函数类型;用户自定义类型包括枚举,结构体和联合体类型。
- 不同位数的操作系统,基本类型所占内存大小也是不同的。比如64位(机器字为8字节)系统
int
类型可能为4 或 8,16位系统int
类型为2字节。 - C++的基本类型则在C语言基本类型基础上,增加了bool类型。
- C语言规定,结构体中各成员列表占用连续内存空间,但结构体的大小并不是简单地将每个结构体成员的大小相加就能得到,需要进行内存对齐,不同编译器默认对齐数不同(VS为8),而对齐倍数由编译器默认对齐数(8) 和 变量类型的字节数(
int
4,char
1)中的最小值(4,1)决定,而对齐数由结构体(int
4,char
1)中最大字节数(4)决定,比如下面结构体中对齐数为4。
即使是64位系统的struct S1 { char a; //1 byte char b; //1 byte int c; //4 byte };
long long
,也只有最多的8 byte
,占一个字长。
数据对齐意味着将数据放在小于或等于字长的倍数的内存偏移处,这由于CPU处理内存的方式而提高了系统的性能。大多数CPU只能访问内存对齐的地址。
操作系统在数据读取的时候,并不是一个字节一个字节进行读取的,而是一段一段进行读取,如果没有对齐约束,代码可能最终不得不跨越机器字边界进行两次或多次访问(有些硬件架构直接报错)。因此考虑平台和性能原因,数据结构(尤其是栈),结构体应该尽可能的在自然边界上对齐,这样访问一个变量只需访问1次内存即可,结构体的内存对齐是拿空间来换取时间的做法。
结构体1和结构体2的成员变量一模一样,可是当我们按照内存对齐规则来计算两个结构体的大小的时候,会发现两个结构体的大小不一样,在VS编译器下(通过对上面结构体的计算,对齐数为4)第一个结构体大小为8(变量起始地址依次为0,1,4,4 + 4 = 8),第二个结构体大小为12(不是9,也不是16,变量起始地址依次为0,4,8,8 + 4 = 12)。解释见结构体中的对齐数到底是什么struct S1 { char a; //1 byte char b; //1 byte int c; //4 byte };//结构体1 struct S2 { char a; //1 byte int c; //4 byte char b; //1 byte };//结构体2
内存对齐更多知识可参考详聊内存对齐(Memory alignment),结构体内存对齐(如何计算结构体的大小),失传的C结构体打包技艺 - 内存连续分配方式包括:单一连续分配,固定分区分配(预先划分分区大小,大小可以不等),动态分区分配(首次适应,最佳适应,最坏适应,邻近适应)。为结构体分配内存空间时采用动态分配 + 内存对齐。
三、运算符与表达式
- 赋值运算符(注意多个变量同时赋值的问题)
- 算数运算符(++i < 100 和 i++ < 100 的区别)
- 关系运算符
- 逻辑运算符(判断是否为闰年,||和&&的使用)
- 逗号运算符,运算符优先级与求值顺序(逗号运算符),可参考C语言逗号表达式在for循环语句中的使用
Note:逗号运算符常用在for循环头部初始化或递增多个变量- 注意变量的处理顺序
- 从左往右开始算
- 整个逗号表达式的值为其中最后一个子表达式的值。
- sizeof运算符(输出 操作数所占空间 的 字节大小),可参考c语言数据类型字节大小 和 精度
Note:sizeof在使用的时候看上去像是一个函数(因为其后面有一对小括号),但他却是一个运算符。他的使用方法有以下3种:- a)
sizeof(变量名)
。 - b)
sizeof(数据类型名)
。 - c)
sizeof 变量名
。
- a)
- 结构体内存对齐1,结构体内存对齐2,可参考c语言数据类型字节大小 和 精度,结构体内存对齐(如何计算结构体的大小),内存管理:固定分区与可变分区,操作系统: 动态(可变)分区分配,内存分配 & C++类对象内存对齐 -------保姆级解释
四、语句与控制流
- 三个数从小到大排序输出(swap原地交换)
- 判断三角形的类型(注意判断条件的先后次序)
- switch-case分数评级
- 九九乘法表打印(9行9列)
- 最大公约数(m)和最小公倍数(q)(ab = mq),可参考辗转相除法求最大公约数
五、函数与程序结构
- 函数的声明和定义(C语言 关注方法的定义相对于调用语句的位置)
Note:其实 python 也一样关注方法的定义相对于调用语句的位置,只不过没有函数声明,只有函数定义。 - 形参与实参(注意swap时实参是引用类型,即传入地址,而形参是指针类型)
- 打印函数调用顺序(C语言(编译)必须通过main函数入口才可以执行程序,python(解释)则不需要)
- 上楼梯问题(C语言模块化设计),可参考C语言模块化编程
- 内部函数(static)和外部函数(extern),可参考C语言中 static 和 extern 的用法详解,C语言关键字static与extern的详细解释
Note:- 头文件(
.h
)需通过#define
和#endif
定义变量和函数 - 源文件可以分成多个
.c
文件,每个.c
文件可以对应一个.h
文件,该.h
文件包含.c
文件的变量或函数声明。 - 全局变量默认是带
extern
,即在整个源程序中 所有 源文件 里都可以访问和修改 static
如果修饰全局变量,则会 对其他源文件隐藏该全局变量
- 头文件(
六、数组(派生数据类型)
Note:
- 数组名,即数组地址,数组元素首地址。 参考C语言:用指针访问数组元素
- 二维数组的存储方式其实在内存中是用一个连续的存储空间存起来的,而二维数组的表示是通过行地址和列地址构成的,第
i
行的地址和第i
行第0列地址的是相同的;参考二维数组的行地址、列地址,与元素的存储int arr[2][5] = {1,2,3,4,5,6,7,8,9,10}; cout << "arr首行地址:" << arr << " ,arr首行首列地址" << &arr[0][0] << endl; cout << "arr第二行地址:" << arr[1] << " ,arr第二行首列地址" << &arr[1][0] << endl; --- arr首行地址:0x40d67ff8f0 ,arr首行首列地址0x40d67ff8f0 arr第二行地址:0x40d67ff904 ,arr第二行首列地址0x40d67ff904
七、指针(派生数据类型,用地址初始化,用*解析地址中的值)
Note:
-
对于指针变量,我们可以把它类比看做是计算机指令,我们从指针变量中取出已存放变量(操作数,操作数组等)的地址(操作数寻址),接着使用
*
对地址进行解析(取操作数);如果是操作数组,则可以通过++
操作实现连续地址空间的访问(获取下一个操作数的地址),但是这样容易出现访问越界问题。 -
假设
a
是一个整型变量,p
为指针变量,指向a
的地址,q
为指针变量,指向p
指针的地址://写法1 int a = 10; //整型变量 int *p = &a; //指针变量,指向整型变量地址 int **q = &p; //二级指针变量,指向指针变量地址 cout << "a值为:" << a << ", a的地址为:" << &a << endl; cout << "p值为:" << p << ", *p为:" << *p << ", *p的地址为:" << &p << endl; cout << "q值为:" << q << ", *q为:" << *q << ", **q为:" << **q << ", q的地址为:" << &q << endl; --- a值为:10, a的地址为:0x4dff1ffbec p值为:0x4dff1ffbec, *p为:10, *p的地址为:0x4dff1ffbe0 q值为:0x4dff1ffbe0, *q为:0x4dff1ffbec, **q为:10, q的地址为:0x4dff1ffbd8
//写法2 int a = 10; //整型变量 int *p; //指针变量 p = &a; int **q; //二级指针变量 q = &p; cout << "a值为:" << a << ", a的地址为:" << &a << endl; cout << "p值为:" << p << ", *p为:" << *p << ", *p的地址为:" << &p << endl; cout << "q值为:" << q << ", *q为:" << *q << ", **q为:" << **q << ", q的地址为:" << &q << endl; --- a值为:10, a的地址为:0x1c577ff97c p值为:0x1c577ff97c, *p为:10, *p的地址为:0x1c577ff970 q值为:0x1c577ff970, *q为:0x1c577ff97c, **q为:10, q的地址为:0x1c577ff968
-
假设
b
是二维数组,p
为指针变量,指向b
的首地址:int row = 2, col = 5; int arr[row][col] = {{1,2,3,4,5},{6,7,8,9,10}}; int *p; p = &arr[0][0]; int num = row * col; int i; for(p = &arr[0][0], i = 1; i <= num; p++, i++){ cout << *p << ","; if(i % col == 0){ cout << endl; } } --- 1,2,3,4,5, 6,7,8,9,10,
-
数组指针是一个指针变量,占有内存中一个指针的存储空间;
int arr[5]={1,2,3,4,5}; int (*p1)[5] = &arr; /*下面是错误的*/ int (*p2)[5] = arr;
指针数组是多个指针变量,以数组的形式存储在内存中,占有多个指针的存储空间。
char *fruits[LEN] = { "apple", "cherry", "grape", "peach", "watermelon" };
假设
arr1
为数组1,arr2
为数组2,arr
为指向arr1
,arr2
的指针数组(arr1
和arr2
长度可以不等)://指针数组(有点像邻接表) int arr1[5] = {1,2,3,4,5}; int arr2[3] = {1,2,3}; int *arr[2] = {arr1, arr2}; int *p = arr[0]; //数组首行指针元素指向的字符串首地址 cout << "arr1数组地址为:" << arr1 << ", arr1数组首元素地址为:" << &arr1 << ", arr1数组首元素值为:" << arr1[0] << endl; cout << "arr[0]值为:" << arr[0] << ", arr数组首地址为:" << arr << ", arr数组首指针元素地址为:" << &arr[0] << ", arr[0]解析的值为" << *arr[0] << endl; cout << "p值为:" << p << ", *p为:" << *p << ", p的地址为:" << &p << endl; int i = 0; for(p = arr[0]; i < 2; i++, p = arr[i]){ cout << "第" << i + 1 << "行: " << endl; do{ cout << "p的地址为:" << p << ", p值为:" << *p << endl; if(*(p + 1) == 0){ break; } //没办法,还是无法解决指针访问地址下标越界 }while(p++); } --- arr1数组地址为:0x52cbfff8a0, arr1数组首元素地址为:0x52cbfff8a0, arr1数组首元素值为:1 arr[0]值为:0x52cbfff8a0, arr数组首地址为:0x52cbfff880, arr数组首指针元素地址为:0x52cbfff880, arr[0]解析的值为1 p值为:0x52cbfff8a0, *p为:1, p的地址为:0x52cbfff878 第1行: p的地址为:0xd32d9ffde0, p值为:1 p的地址为:0xd32d9ffde4, p值为:2 p的地址为:0xd32d9ffde8, p值为:3 p的地址为:0xd32d9ffdec, p值为:4 p的地址为:0xd32d9ffdf0, p值为:5 p的地址为:0xd32d9ffdf4, p值为:565 p的地址为:0xd32d9ffdf8, p值为:16 第2行: p的地址为:0xd32d9ffdd4, p值为:1 p的地址为:0xd32d9ffdd8, p值为:2 p的地址为:0xd32d9ffddc, p值为:3 p的地址为:0xd32d9ffde0, p值为:1 p的地址为:0xd32d9ffde4, p值为:2 p的地址为:0xd32d9ffde8, p值为:3 p的地址为:0xd32d9ffdec, p值为:4 p的地址为:0xd32d9ffdf0, p值为:5 p的地址为:0xd32d9ffdf4, p值为:565 p的地址为:0xd32d9ffdf8, p值为:16 p的地址为:0xd32d9ffdfc, p值为:1 p的地址为:0xd32d9ffe00, p值为:1
指针数组中的每一个元素(
arr[0]
,arr[1]
),其实是各个一维数组的首地址(可以初始化,也可以malloc
),不是指针变量的地址,指针变量地址应为&arr[0]
,&arr[1]
,指针变量只是指向了这些数组的首地址,避免了空指针问题。
字符指针数组 的使用参考【C语言】字符串数组按字典升序,C语言指针数组初始化 -
*
和&
是相反操作:ptr = &fruits[0]; *ptr = fruits[0]
,这里要注意指针数组也是数组,依然满足指针数组的数组名(fruits
)为第一个指针元素的首地址(&fruits[0]
),但不等于fruits[0]
。参考交换变量值2(我的题解)//定义指针数组 char *fruits[LEN] = { "apple", "cherry", "grape", "peach", "watermelon" }; //等价于 char **ptr = fruits; 也等价于 char **ptr = &fruits[0]; char **ptr; ptr = &fruits[0]; //指针地址,而*ptr为指针地址中存储的值,首行第一个字符串的首地址 for (int i = 0; i < LEN; ++i, ptr++) { printf("%s\n",*ptr); //ptr表示fruits首行第一个指针元素的地址, *ptr表示fruits首行第一个指针元素的值,即首行第一个字符串的首地址。 } --- apple cherry grape peach watermelon
-
*p++
、*(p++)
、(*p)++
、*++p
、++*p
的运算规则 参考指针&指针值的自增与自减(结合自增运算符 A = i++, B = ++i 理解) -
原则上指针变量可以从地址的角度,访问所有变量(基本类型变量,数组,函数),操作所有变量;但是实际上对于指针的使用,还是得结合特定场景(链表实现等),不要一味追求指针,否则很容易弄巧成拙。因为任何变量类型可以转换成指针类型,但是指针类型不能转化成指定的变量类型,比如字符串等价于字符数组,而不等于字符指针(下一条有说明)。
-
数组和指针是不同的数据类型,有本质的区别:
char str[] = "abcd"; //sizeof(str) == 5 * sizeof(char)
,而char *str = "abcd"; //sizeof(str) == 4(x86) or 8(x64)
。
char *str = "xxxxx"
,str指向该常量地址(静态存储区);char str[] = "xxxxx"
,str在栈上申请空间,将常量内容复制进来,所以是局部变量,允许字符串strcpy
操作;参考char str[] 和 char *str 的区别,C 语言 strcpy 报 segmentation fault,动态存储区、静态存储区、堆和栈的区别 -
指针比较难理解,在了解基础概念之后一定要多加练习,不然很容易整迷糊。
八、字符串
- 字符串输入与输出(gets和puts),参考C++的gets和puts
Note:char *tail = str; while(*tail++)
; 当地址溢出时,地址中的值为负值,跳出循环;tail -= 2;
即回到字符串倒数第二个地址的位置(倒数第一个是'\0'
)
- 字符串函数的使用(strcat,strcmp,strcpy),参考strcat、strcpy、strcmp三种函数用法,C 语言 strcpy 报 segmentation fault,动态存储区、静态存储区、堆和栈的区别
Note:char *src
和char src[]
虽然都可以表示字符串,但是数据存储地方不一样,char *src = "abcd"
等价于const char *src = "abcd"
,指针数组存放在静态存储区,即常量区,该区只许读不许写,因此字符指针不能使用strcpy
方法,虽然在编译时没有问题,但是在运行时会报错。
- 字符串的输入输出可参考C语言:字符串数组与字符串指针数组,C语言:字符数组的输入输出,C语言字符串输入及输出的几种方式,【c语言】malloc函数详解
九、结构体(用户自定义数据类型)
- 结构体的定义与使用,参考结构体定义 typedef struct 用法详解和用法小结
- 结构体数组,结构体与函数(打印结构体数组对象),参考结构体&结构体数组
- 结构体指针访问(‘.‘优先级高于’*’),参考C语言运算符优先级
- 结构体指针类型:用链表保存一个班级所有学生的基本信息,参考如何定义结构体指针
Note:
- 声明和初始化结构体的几种方式:注意结构体声明时末尾带
;
//声明结构体方法1:只能创建一个实例stu;struct为结构体类型符,Student是结构体的类型名;最后的分号一定要写 struct Student { char *name; int id; unsigned int age; char group; float score; } stu = {"张三", 1001, 16, 'A', 95.50};
//声明结构体方法2:typedef将结构体`struct Student`重命名为student或者Stu,其中`struct Student`可以简写为`struct`; typedef struct Student { char *name; int id; unsigned int age; char group; float score; }student,Stu; //初始化方法1(struct关键字可省略) Student stu = {"张三", 1001, 16, 'A', 95.50}; //初始化方法2 Stu stu1 = {"张三", 1001, 16, 'A', 95.50}; //初始化方法3(在main函数中为结构体成员赋值) struct Stu stu2; int main(int argc, char** argv) { stu2.name = "张三"; stu2.id = 1001; stu2.age = 16; stu2.group = 'A'; stu2.score = 95.50; printf("========== 学生基本信息 ==========\n"); printf("姓名:%s\n学号:%d\n年龄:%d\n所在小组:%c\n成绩:%.2f\n", stu2.name, stu2.id, stu2.age, stu2.group, stu2.score); printf("==================================\n"); return 0; }
- 如果使用结构体声明方法1来定义结构体数组,而不是通过结构体名定义结构体数组时,只能在结构体定义时同时对数组初始化:
错误做法如下,会报编译错误:error: assigning to an array from an initializer list
正确做法如下#include <stdio.h> #define NUM_STR 3 struct Student { char *name; int id; unsigned int age; char group; float score; } cls[NUM_STR]; int main(int argc, char** argv) { cls = { {"张三", 1001, 16, 'A', 95.50}, {"李四", 1002, 15, 'A', 90.00}, {"王五", 1003, 16, 'B', 80.50} }; return 0; }
#include <stdio.h> #define NUM_STR 3 struct Student { char *name; int id; unsigned int age; char group; float score; } cls[] = { "张三", 1001, 16, 'A', 95.50, "李四", 1002, 15, 'A', 90.00, "王五", 1003, 16, 'B', 80.50 }; int main(int argc, char** argv) { return 0; }
- 指针访问对象 :
student->name
或(*student).name
,不能是student++
。 - 想要定义结构体类型的指针一定要用
typedef
十、联合体与枚举类型(用户自定义类型)
Note:
- 定义枚举类型:
1)格式:enum 枚举类型 {枚举值列表}
;
2)枚举值列表可以手动设置,也可以默认从0递增;
3)如果在全局下定义枚举类型,则枚举类型中所有的枚举值均为全局变量//1、定义枚举类型: //枚举颜色(red, orange, yellow, green, ching, blue, purple为枚举类型变量,如果在全局下定义枚举类型,则枚举类型中所有的枚举值均为全局变量,值为整型;如果有指定enum值,则将指定的enum值赋予enum变量) enum color{red=1, orange=2, yellow=3, green=4, ching=5, blue=6, purple=7}; //枚举一个星期的每一天(默认从0开始递增得到enum值) enum week { Su, Mo, Tu, We, Th, Fr, Sa }; //枚举每一个月 enum month { January, February, March, April, May, June, July, August, September, October, November, December }; int main(int argc, char const *argv[]) { printf("%-3d %-3d %-3d %-3d %-3d %-3d %-3d\n", red, oreange, yellow, green, ching, blue, purple); printf("%-3d %-3d %-3d %-3d %-3d %-3d %-3d\n", Su, Mo, Tu, We, Th, Fr, Sa); printf("%-3d %-3d %-3d %-3d %-3d %-3d %-3d %-3d %-3d %-3d %-3d %-3d\n", January, February, March, April, May, June, July, August, September, October, November, December); return 0; } --- 1 2 3 4 5 6 7 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 10 11
- 定义枚举类型变量:
//枚举一个星期的每一天(默认从0开始递增得到enum值) enum week { Su, Mo, Tu, We, Th, Fr, Sa }; //定义枚举类型的变量 enum week a = Su; enum week b = Mo; enum week c = Fr; int main(int argc, char const *argv[]) { printf("%-3d %-3d %-3d\n", a, b, c); return 0; } --- 0 1 5
十一、位运算
- 尾数加一(利用位运算>>和 & 将十进制打印成二进制,利用 原十进制数 和 加1的十进制数 按位与 得到问题的解),可参考位运算(a << b 左移,末尾添b个0;a >> b 右移,去掉末b位;&按位与)
- 整数 x 是否是 2 的正整次幂(按位与)
- 计算整数的二进制位数(右移1位)
- 正数转八进制(右移3位)
- 结构体内存对齐1,结构体内存对齐2,可参考c语言数据类型字节大小 和 精度,结构体内存对齐(如何计算结构体的大小),内存管理:固定分区与可变分区,操作系统: 动态(可变)分区分配,内存分配 & C++类对象内存对齐 -------保姆级解释
- 位字段(bit filed),可参考位域的使用详解(内存对齐)