0%

C语言学习笔记

这篇是学习笔记, 因为我决定最近专门重新学习一下C语言.

经常有人说自己入门C语言时觉得C语言很简单, 不到一个月就能入门, 但用久了会觉得C语言很难, 觉得自己还没有入门. 而我现在觉得当时速成的C语言已经全部不记得了... 虽然我目前并没有使用C的需求, 但总是看到各种C系代码, 一直头疼也不是办法, 不如温故而知新, 再次"速成"一下.

语句与行

C语言我感觉对格式要求蛮宽松的, 可以一行多个语句 (每个语句必须以;结尾), 也可以跨行写语句(在行末写\折行), 不像python对缩进都有严格要求.

🌟 代码风格的话我只认VS风, 就是Visual Studio格式化出来的样式. 是的我是大括号换行派!

💡 要注意的是预编译指令必须一行不能写多个语句, 但可以折行.

注释

有两种方式

推荐的方式

👇这种注释可以跨行写, 也可以在行内写

1
2
3
4
#include /*comment*/ <stdio.h>
/*comment
comment
comment*/

💡值得一提的是只要出现 /* 就会被识别为注释的开始, 如果想把y除z指向的内存的值赋给x写成下面这样是不行的.

1
x = y/*z

但这个问题也很好解决, 用空格或括号就能解决.

1
2
x = y / *z
x = y/(*z)

不推荐的方式

这样只能单行注释

1
code  // comment

❗️要注意这种注释方式是借鉴自C++, 在C99中才被标准化, 而目前使用最广泛的是C89标准, 也就是说不使用是比较保险的.

内存

C语言对内存的操作似乎很多, 也很注重内存管理.

C语言为内存的分配和管理提供了几个函数. 这些函数可以在 <stdlib.h> 头文件中找到.

void *malloc(size_t size) 分配一块size大小的内存

void *calloc(size_t num, size_t size) 分配一块储存了一个num长, 每个元素 size字节的内存并将所有位初始化为0

void *realloc(void *ptr, size_t size) 重新分配内存, 把内存扩展到 newsize

void free(void *ptr) 释放ptr所指向的内存

💡 malloc是memory allocate, realloc是reallocate, calloc不知道是啥.

段与栈与堆

(segmentation)是指二进制可执行文件内的区域,所有某种特定类型信息被保存在里面.可以用size命令得到可执行文件中各个段的大小. 正文段 (Text Segment)用于储存指令, 数据段 (Data Segment)用于储存已初始化的全局变量, BSS段 (BSS Segment)用于储存未赋值的全局变量所需的空间.

调用栈并不储存在可执行文件中,而是在运行时创建.调用栈所在的段称为堆栈段(Stack Segment). 和其他段一样, 堆栈段也有自己的大小, 不能被越界访问, 否则就会出现段错误 (Segmentation Fault). 这种情况叫栈溢出.

栈空间有多大和操作系统相关. 在Linux中, 栈大小是由系统命令ulimit指定的, 例如 ulimit -a显示当前栈大小, 而ulimit -s 32768将把栈大小指定为32MB. 但在Windows 中, 栈大小是储存在可执行文件中的. 使用gcc可以这样指定可执行文件的栈大小: gcc -Wl,--stack=16777216, 这样栈大小就变为16MB

💡 栈溢出不一定是递归调用过多导致的, 也可能是局部变量太大. 因此较大的数组建议储存为全局变量.

是由编译器在需要时分配的, 不需要时自动清除的变量存储区. 里面的变量通常是局部变量, 函数参数等. 是由malloc()函数分配的内存块,内存释放由程序员手动控制, 在C语言由free()完成.

数组

较大的数组应在main函数之外声明.

memset(a, 0, sizeof(a))能方便地把数组a清零,这个函数在string.h里.

strcpy(a, b), strcmp(a, b), strcat(a, b)来执行“赋值”、“比较”和“连接”操作, 这三个函数也在 string.h

字符串

C语言中的字符串是以“\0”结尾的字符数组

不同操作系统的回车换行符是不一致的. Windows是“和“”两个字符,Linux是“”,而 MacOS是“. 如果在Windows下读取Windows文件, fgetc和getchar会把“"吃掉”, 只剩下“”; 但如果要在Linux下读取同样一个文件, 它们会忠实地先读取“, 然后才是“”.

很有意思的一个小问题: "5", '5' 和 5 有什么区别?
"5"是一个字符串, '5'是一个字符常量, 5是一个数字常量

指针

用int* a声明的变量a是指向int型变量的指针. 赋值a = &b的含义是把变量b的地址存放在指针a中, 表达式a代表a指向的变量, 既可以放在赋值符号的左边(左值), 也可以放在右边 (右值) 注意: a是指“a指向的变量”, 而不仅是“a指向的变量所拥有的值”. 理解这一点相当重要. 例如, a = a + 1就是让a指向的变量自增1. 甚至可以把它写成(a)++. 注意不要写成a++, 因为++运算符的优先级高于取内容运算符 *, 实际上会被解释成*(a++).

指针的运算

  • 一个指针变量加/减一个整数是将该指针变量的原值(是一个地址)和它指向的变量所占用的内存单元字节数相加或相减.
  • 两个指针变量间可以做减法, 但前提是这两个指针是指向同一个数组的元素. 两指针变量差是两个指针之间的元素个数
  • 如果两指针变量指向同一数组的元素, 他们可以进行比较运算. 另外所有指针都可以和 NULL进行相等/不想等比较.

预处理

宏定义

1
#define 宏名 字符串

宏名一般为大写, 以下划线连接单词.

看了网上资料我感觉在C中宏定义主要是以下作用

增加代码抽象性

用宏定义替代magic number, 或者嵌入式中一些寄存器的位操作, 一方面增强代码可读性 (不像魔数让人不明所以, 而寄存器的位操作也很不直观), 一方面增强代码可移植性 (比如从一种单片机移植到另一种单片机, 只需更改宏定义即可)

防止重复定义

1
2
3
4
#ifndef XXXX
#define XXXX
...
#endif

在头文件里用这样的语句来防止头文件被重复引用. 有一些头文件被重复引用会增加编译器工作, 降低编译效率, 而有一些头文件被重复引用会引起冲突 (比如如果头文件里定义了全局变量, 会发生重复定义错误).

控制代码编译

通过Makefile控制编译选项.

还能像轮子哥这样当模板用.

❗️虽然宏定义的好处不少, 但除了以上情况尽量少用尽量不用宏定义, 因为这会让开发者看到的代码与编译器看到的代码不同, 容易导致想不到的问题.

调试技巧

调试时重点关注两方面: 当前行的跳转, 变量的变化

循环结构程序设计中最常见的两个问题: 算术运算溢出, 程序效率低下

变量在未赋值之前的值是不确定的.

gcc与gdb命令简记

gcc

常用选项:

name_of_option 我猜的全名 含义
-o [filename] output 指定输出文件名
-g gdb 生成调试用的符号表
-Wall warning all This enables all the warnings about constructions that some users consider questionable, and that are easy to avoid (or modify to prevent the warning), even in conjunction with macros.
-lm link math.h 链接math.h. C++编译器会自动链接, 但C的代码使用了math.h却不启动这个选项很可能出错
-ansi ANSI 检查代码是否符合ANSI标准 (常与-Wpedantic连用)
-Wpedantic warning pedantic Issue all the warnings demanded by strict ISO C and ISO C++; reject all programs that use forbidden extensions, and some other programs that do not follow ISO C and ISO C++. For ISO C, follows the version of the ISO C standard specified by any -std option used.
-O1, -O2, -O3 optimize 开启速度优化. 开启后编译出的程序比直接编译出的程序快, -O2比-O1快, -O3比-O2快, 但为了避免优化误解代码含义, 在算法比赛中推荐-O2. 当然如果程序十分规范就没有这种担心.
-DXX define XX 在编译时定义XX符号 (此处XX是随意什么大写单词的意思), 位于#ifdef XX和#endif中间的语句会被编译

gcc与g++

gcc到底能不能编译C++程序?

在某种程度上gccg++都可以编译 .cpp 后缀的程序, 但是gcc命令不能自动和 C++程序使用的库链接. gcc把后缀为 .c 的当作是C程序,而g++将其当作C++程序. 两者都会将后缀为 .cpp 的程序视作C++程序, 要注意虽然C++是C的超集,但是两者对语法的要求是有区别的, C++的语法规则更加严谨一些. 编译阶段, g++会调用gcc,因为 gcc命令不能自动链接C++库, 所以通常用g++来完成链接, 统一起见,干脆编译和链接统统用g++了, 这就给人一种错觉, 好像cpp程序只能用g++似的.

gdb

💡执行gdb时加选项-q (quiet)可以去掉进入gdb开头的废话.

常用命令:

命令 全名 含义
l list 列出十行代码. 但可以通过set listsize来更改显示多少行, 用show listsize能查看listsize. l后可以接行号, 函数名
r run 开始运行程序
b break 设置断点, b后接行号或函数名
c continue 继续运行. 要注意在断点处停下后用c继续而不是r
n next 下一行
s step 与n的区别是n会执行完本行语句, 而有函数调用时s会停在函数内
u until 执行到指定行号或者指定函数的开头
i info 显示各种信息. 如i b显示所有断点,i disp显示display,而i lo显示所有局部变量
disp display 把一个表达式设置为display, 当程序每次停下来时都会显示其值
cl clear 取消断点, 和b的格式相同. 如果该位置有多个断点, 将同时取消
cond condition 用来设置条件断点
ig ignore 设置记次断点, count次以前不停止
wa watch point watch a(简写为wa a)可以在变量a修改时停下,并显示出修改前后的变量值
aw all watch point 读写时都停下
rw read watch point 被读取时停下
q quit 退出gdb
感谢您的认可!