C 语言笔记
学习 C 语言过程中整理的笔记,记录了对各个知识点的理解和练习。
基础操作环境与 HelloWorld
常用系统命令
ls # 查看当前目录下文件信息
ls -l # 查看文件详细信息
ls -a # 查看当前目录中所有文件,包含隐藏文件
cd # 切换到指定的目录,回车默认回到家目录
cd /root/xxx # 切换到指定目录
cd .. # 回到上级目录
cd - # 回到上次所在的工作目录
cd ../../ # 返回到上级的上级目录
touch xxx # 创建一个普通文件
mkdir xxx # 创建一个目录文件
pwd # 查看当前工作目录的绝对路径
rm xxx # 删除指定的文件
编写第一个 C 程序
vim a.c # 创建并打开源文件
# 按下 i 键进入编辑模式,写 C 代码
# 按下 ESC 键退出编辑模式
# 按下 shift+zz 保存并退出
gcc a.c # 编译
./a.out # 运行
计算机基础
计算机的基本结构:输入 → 输出 → 运算器 → 存储器 → 控制器
编程语言发展:机器语言 → 汇编语言 → 高级语言(面向过程 C → 面向对象 C++/Java → 面向组件)
基本数据类型
用 sizeof() 可以测试一个数据类型的内存占用字节数。
逻辑类型
#include <stdbool.h> // 引入布尔类型头文件
bool flag = true; // 非0即真,0为假
整数类型
| 类型 | 字节 | 范围(有符号) |
|---|---|---|
char | 1 | -128 ~ 127 |
short | 2 | -32768 ~ 32767 |
int | 4 | -2^31 ~ 2^31-1 |
long | 4/8 | 取决于平台 |
long long | 8 | -2^63 ~ 2^63-1 |
int是测试机器位数的标准类型。在 16 位机器上int == short,32 位机器上long == int。
实数类型(浮点数)
| 类型 | 字节 | 精度 |
|---|---|---|
float | 4 | 6位有效数字 |
double | 8 | 15位有效数字 |
浮点数不能直接用
==与 0 比较,需要在误差范围内比较:
#define EPS 0.000001
if (-EPS <= a && a <= EPS) // 判断 a 是否等于 0
if (a > EPS) // 判断 a 是否大于 0
if (a < EPS) // 判断 a 是否小于 0
常量与变量
常量分类
| 类型 | 示例 |
|---|---|
| 十进制整数 | 123, 789 |
| 八进制 | 01, 0777 |
| 十六进制 | 0x1, 0x45ad |
| 字符常量 | 'A', 'c' |
| 字符串常量 | "abc", "hello" |
| 指数常量 | 1.36e+10 (= 1.36×10^10) |
| 标识符常量 | #define PI 3.14 |
网络配置备忘
# 查看与配置 IP
ifconfig
ifconfig eth0 192.168.1.xxx/24 up
ping 192.168.1.199
# 如果网络不可达,添加默认网关
route add default gw 192.168.1.1
# NFS 客户端挂载
mkdir /mnt/jsj
mount 192.168.1.199:/mnt/nfs /mnt/jsj
cp /mnt/jsj/study /root/ -r
umount /mnt/jsj
# 关闭防火墙
service iptables stop
# NFS 服务端
service nfs restart
vi /etc/exports # 修改配置
service nfs restart
service iptables stop
运算符
算术运算符
+ - * / % ++ --
int a = 8, b = 9;
int c = a + b; // 加法
int c = a - b; // 减法
int c = a * b; // 乘法,注意越界
int c = a / b; // 除法,结果默认取整
int c = a % b; // 取余(取模)
除法运算:如果希望得到浮点结果,需要对操作数做
float强制类型转换。0不能做分母。
自加自减
int a = 8;
a++; // 先使用 a 的值,后加 1
++a; // 先加 1,再使用 a 的值
a--; // 先使用,后减 1
--a; // 先减 1,后使用
逻辑运算符
&& || ! // 与、或、非
逻辑与 &&:双目运算符,类似算术乘法
| 左 | 右 | 结果 |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 0 |
短路原则:
a && b && c && d中只要有一个为假,后续不再判断。
逻辑或 ||:类似算术加法
| 左 | 右 | 结果 |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
短路原则:只要有一个为真,后续不再判断。
逻辑非 !:单目运算符,取反
int a = 0;
!a; // 真 (1)
int b = 1;
!b; // 假 (0)
位运算符
& | ^ ~ >> <<
// 位与 位或 异或 取反 右移 左移
位与 &
int x = 10, y = 3;
int z = x & y; // z = 2
// x=10: 0000 1010
// y=3: 0000 0011
// ----------------
// z=2: 0000 0010
位或 |
unsigned char x = 076, y = 0x89; // x=0x3E, y=0x89
z = x | y; // z = 0xBF = 191
寄存器置位与清零
// 第3bit置0:用 &
int a = 0xAE; // 1010 1110
int c = a & 0xAA; // 1010 1010 = 0xAA
// 第5bit置1:用 |
int a = 0xAE; // 1010 1110
int c = a | 0x10; // 1011 1110 = 0xBE
异或 ^:相同为0,不同为1
| 左 | 右 | 结果 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
移位 >> <<
unsigned char a = 0xE4, b, c; // a = 1110 0100
b = a << 3; // b = 0010 0000 = 0x20
c = b >> 3; // c = 0000 0100 = 0x04
// 将变量的第4bit置1
int x = 0xAE;
x = x | (1 << 3);
左移等价于乘以 2^n,右移等价于除以 2^n。
特殊运算符
条件运算符(三目)
z = (a > b) ? 100 : -100;
// 等价于
if (a > b) { z = 100; }
else { z = -100; }
逗号运算符
z = (x=x+5, y=x+0.2); // z 取最后一个表达式的值
sizeof 运算符
sizeof(int); // 计算类型占用的字节数
sizeof(variable); // 计算变量占用的字节数
SSH 远程登录
# 服务端
service sshd restart
rpm -ivh openssh*
# 客户端
ssh root@192.168.1.xxx
# 输入密码后即登录到对方机器,用 exit 退出
逻辑控制语句
顺序语句
程序按照代码书写顺序依次执行,类似流程图。
分支语句
if…else
if (表达式) {
语句块1;
} else {
语句块2;
}
注意事项:
- 只有一条语句时可省略
{} else可选,但有else必须有if- 嵌套不要超过四层
- 支持阶梯形式:
if...else if...else
switch 多分支
switch (整数表达式) {
case 常量1: 语句1; break;
case 常量2: 语句2; break;
default: 语句n;
}
执行流程:计算表达式值 → 依次匹配
case→ 匹配成功则执行对应语句 →break跳出。如果不加break,会继续执行后续case(穿透)。
循环语句
for 循环
for (表达式1; 表达式2; 表达式3) {
循环体;
}
// 示例
for (i = 0; i < 100; i++) {
printf("i = %d\n", i);
}
三个表达式都可以省略,但
;不能省。for(;;)是死循环。
while 循环
while (表达式) {
循环体;
}
// 死循环
while (1) {
// ...
}
建议用
while(1)写死循环而非for(;;)。循环中避免空转。
do…while 循环
do {
循环体;
} while (表达式); // 注意分号
先执行后判断,保证循环体至少执行一次。
goto 跳转
flag:
i = 1;
sum = sum + i;
goto flag;
辅助控制关键字
| 关键字 | 作用 |
|---|---|
break | 跳出当前循环或 switch |
continue | 跳过本次循环剩余部分,从头继续 |
return | 结束当前函数(main 中即结束程序) |
练习:成绩等级判断
switch (score / 10) {
case 10:
case 9: printf("A\n"); break; // 90-100
case 8: printf("B\n"); break; // 80-89
case 7: printf("C\n"); break; // 70-79
case 6: printf("D\n"); break; // 60-69
default: printf("E\n"); // 0-59
}
循环控制语句
do…while
do {
循环体;
} while (表达式);
执行流程:先执行 → 后判断。如果为真继续循环,为假退出。
// 练习:用 do...while 求 100 以内偶数和
int i = 0, sum = 0;
do {
sum += i;
i += 2;
} while (i <= 100);
goto 跳转
// 练习:用 goto 求 100 以内整数和
int i = 1, sum = 0;
loop:
sum += i;
i++;
if (i <= 100) goto loop;
数组基础
定义
int a[10]; // 定义一个包含 10 个 int 的数组
要点:
- 数组是构造数据类型,所有元素类型必须相同
- 下标从
0开始到n-1,不要越界(a[n]错误,a[n-1]正确) - 数组元素在内存中连续存储
- 局部数组未初始化时值为随机值
初始化
int a[10]; // 不初始化:栈区随机值,静态区为0
int a[10] = {1, 2, 3}; // 部分初始化
int a[5] = {11, 22, 33, 44, 55}; // 全部初始化
int a[10] = {0}; // 默认初始化为0
int a[] = {1, 2, 3}; // 不定长初始化,数组大小由数据决定
遍历
// for 循环遍历
for (i = 0; i < n; i++) {
printf("%d\n", a[i]);
}
// while 循环遍历
while (i < n) {
printf("%d\n", a[i]);
i++;
}
排序算法
冒泡排序
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
选择排序
for (i = 0; i < n - 1; i++) {
min = i;
for (j = i + 1; j < n; j++) {
if (a[min] > a[j]) {
min = j;
}
}
if (min != i) {
tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
}
二维数组与字符数组
二维数组
定义
int a[3][4]; // 3行4列
初始化
int a[3][4] = {{0}}; // 默认初始化为0
int a[3][4] = {{1,2,3,4}, {9,8,7,6}}; // 局部初始化
int a[2][2] = {{1,2}, {5,6}}; // 全部初始化
int a[][3] = {{1,2,3}, {7,8,9}}; // 不定长:行可省,列不可省
遍历
// 双层 for 循环按行列遍历
for (i = 0; i < 行; i++) {
for (j = 0; j < 列; j++) {
printf("%d ", a[i][j]);
}
}
字符数组
定义与初始化
char c[10]; // 定义
char c[10] = {'a', 'b', 'c'}; // 逐个赋值
char c[10] = "abc"; // 字符串常量赋值
字符串是字符数组的特殊形式:在字符结尾多一个
'\0'作为结束标记。
常用字符串函数(string.h)
size_t strlen(const char s[]); // 计算字符串有效长度
char *strcpy(char *dest, const char *src); // 字符串拷贝
int strcmp(const char *s1, const char *s2); // 字符串比较
char *strcat(char *dest, const char *src); // 字符串拼接
void bzero(void *s, size_t n); // 内存清零
void *memset(void *s, int c, size_t n); // 内存设置为指定值
strlen计算从开始到第一次出现'\0'的字符个数。sizeof计算数组实际大小,与存储内容无关。
strcmp 返回值
| 条件 | 返回值 |
|---|---|
| s1 == s2 | 0 |
| s1 > s2 | >0 |
| s1 < s2 | <0 |
gets/puts
char *gets(char *s); // 从终端获取字符串输入(有溢出风险)
int puts(const char *s); // 输出字符串并换行
gets存在缓冲区溢出漏洞,不建议在不确定数据长度的情况下使用。
指针与数组
指针基础
定义
char *pc; // pc 存储一个地址,该地址对应的内存可放一个 char
int *pa; // pa 存储一个地址,该地址对应的内存可放一个 int
数据类型:标识目标存储空间大小*:标识该类型是指针变量名:存储地址的值
取地址与间接访问
int a = 100;
int *pa = &a; // &a:取 a 的地址
*pa = 200; // *pa:通过地址访问 a,效果同 a = 200
指针的初始化
char *pc = NULL; // NULL 初始化,避免野指针
char *pc = &c; // 变量地址初始化
char *pc2 = pc; // 用其他指针初始化
char *pc = (char *)0x1234; // 绝对地址初始化(必须可访问)
指针的运算
算术运算
px + n; px - n; px++; px--;
运算时自动以指针指向的类型大小为步长(即
px+1实际是地址值 + sizeof(类型))。
关系运算
if (NULL == px) { } // 检测指针是否为空
if (px > py) { } // 比较地址大小
赋值运算
pc2 += 1; // pc2 = pc2 + 1
pc2 -= 1; // pc2 = pc2 - 1
指针与数组的关系
int a[5] = {1, 2, 3, 4, 5};
int *pa = &a[0]; // 或 int *pa = a;
// 等价访问公式:
a[i] == pa[i] == *(pa + i) == *(a + i)
数组名是指针常量(不能
a++),指针是指针变量(可以pa++)。
char 数组 vs char 指针
char buff[] = "abc"; // buff 在栈区,内容可修改
char *pa = &buff[0]; // pa 指向本地内存,可修改
char *pb = "def"; // pb 指向静态常量区,内容不可修改!
指针经典问题
int a[5] = {1, 2, 3, 4, 5};
int *p = (int *)(&a + 1);
printf("%d %d\n", *(a + 1), *(p - 1)); // 输出:2 5
a是数组名(指向首元素),&a是整个数组的地址(跨度为整个数组大小),&a+1跳到数组末尾之后。
特殊类型指针
const 指针
原则:去掉类型后距离 const 最近的修饰部分为常量。
const char *p; // *p 不可修改,p 可指向其他地址
char const *p; // 同上
char * const p; // p 不可指向其他地址,*p 可修改
const char * const p; // 都不可修改
void 指针
void *p; // 纯地址变量,没有具体类型指向,占 4 字节
任何地址类型可自动转为
void *,但void *转为具体类型必须强制转换。没有void类型的变量。
二级指针
char **pa; // char* *pa,可以理解为"字符串的指针"
char *arr[] = {"abc", "def", "ghi"};
char **p = &arr[0]; // p 指向字符串数组
// *p = "abc", **p = 'a'
函数基础
定义格式
<存储类型> <数据类型> 函数名(形参列表) {
函数体;
}
示例
int add(int a, int b) {
return a + b;
}
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
传参方式
| 方式 | 说明 | 示例 |
|---|---|---|
| 传值 | 拷贝数据,不改变原变量 | add(a, b) |
| 传地址 | 传递指针,可修改原变量 | swap(&a, &b) |
定义与声明
// 声明(通知编译器函数存在)
int add(int, int);
int add(int a, int b);
// 定义(有函数体)
int add(int a, int b) {
return a + b;
}
声明必须在调用之前,声明时形参名可省略但类型不可省略。
返回值约定
return 0; // 成功
return -1; // 失败(非0值表示错误码)
结构体、联合体与枚举
结构体 struct
定义
struct student {
int id;
char name[32];
short age;
float height;
};
// 无名结构体(临时单次使用)
struct {
int id;
char name[32];
} var1, var2;
// 嵌套结构体
struct zhuanye {
int zy_id;
char zy[128];
struct student stu; // 必须先有 student 的定义
};
变量定义与初始化
struct student stu; // 定义变量
struct student stu1 = {100, "zhangsan", 20, 1.75f}; // 全部初始化
struct student stu2 = {100, "zhangsan"}; // 部分初始化
struct student stu3 = {.name = "lisi", .age = 20}; // 指定初始化
struct student stu4 = {0}; // 默认初始化(清零)
结构体指针
struct student stu1;
struct student *pstu = &stu1;
// 访问成员变量
stu1.id; // 直接引用:变量名.成员
pstu->id; // 间接引用:指针->成员 等价于 (*pstu).id
结构体作为函数参数
// 传值(拷贝整个结构体,不改变原数据)
void print_student(struct student stu) { }
// 传地址(传递指针,可修改原数据,效率更高)
void input_student(struct student *stu) { }
结构体作为返回值
struct student create_student() {
struct student s;
// ...
return s; // 值返回
}
struct student *create_student2() {
// 注意:不能返回局部变量的地址!
}
结构体有字节对齐问题。Linux 平台下以 4 的倍数向 2^n 次幂靠拢。如果结构体中有指针,初始化后不能重新赋值修改(会段错误)。
联合体 union
所有成员共用同一段内存,修改一个成员会影响其余所有成员。占用内存等于最长成员的大小。
union data {
int n; // 4 字节
char ch; // 1 字节
short m; // 2 字节
};
// sizeof(union data) = 4(等于最大成员 n)
演示代码
#include <stdio.h>
union data {
int n;
char ch;
short m;
};
int main() {
union data a;
printf("%d\n", sizeof(a)); // 输出: 4
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 40, @, 40
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 39, 9, 39
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 2059, Y, 2059
return 0;
}
成员 n、ch、m 在内存中对齐到同一头部:对 ch 赋值影响第1字节,对 m 赋值影响前2字节,对 n 赋值影响全部4字节。
实用场景:学生/教师信息表,根据职业类型 s(学生)或 t(教师)存储不同数据。
枚举 enum
枚举在编译阶段将名字替换为对应值,本质是编译阶段的宏,值不占用数据区内存。
enum week { Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
// Mon=1, Tues=2, Wed=3, ...
enum week day = Mon;
switch (day) {
case Mon: puts("Monday"); break;
case Tues: puts("Tuesday"); break;
// ...
}
sizeof(enum week) == sizeof(int) == 4
内存管理
C 语言使用 4G 虚拟内存,分为三个区域:
| 区域 | 存储内容 | 生命周期 |
|---|---|---|
| 静态存储区 | 全局变量、static 变量 | 程序运行期间一直有效 |
| 栈区 | 局部变量(auto) | 函数结束即释放 |
| 堆区 | 手动申请的内存 | 手动释放 |
申请内存
#include <stdlib.h>
void *malloc(size_t size);
char *pc = (char *)malloc(400000);
if (NULL == pc) {
return -1; // 申请可能失败
}
bzero(pc, 400000); // 申请成功的内存不一定干净,需要清零
释放内存
void free(void *ptr);
free(pc); // malloc 和 free 必须成对出现,防止泄漏
常见问题:越界访问导致段错误;多次 free 同一地址导致崩溃;
pc++后 free 造成泄漏。用valgrind --tool=memcheck ./a.out检测内存问题。
课后练习代码
a.c
#include <stdio.h>
int main(){
printf("hello world \n");
return 0;
}
b.c
#include <stdio.h>
int main() {
int a = 1;
int b = 1;
int c = a + b;
printf("c = %d \n",c);
return 0;
}
test1.c
#include <stdio.h>
int main(){
printf("%d\n",sizeof(short));
return 0;
}
test2.c
#include <stdio.h>
int main(){
char x = 128;
printf("x = %d \n",x);
unsigned char y = 256;
printf("y = %u \n",y);
return 0;
}
test3.c
#include <stdio.h>
int main(){
// printf("hello world \n");
short x = -32769;
printf("x = %d \n",x);
return 0;
}
test4.c
#include <stdio.h>
int main(){
// printf("wo shi test4 \n");
unsigned int x = -1;
printf("x = %u \n",x);
printf("int sizeof = %d \n",sizeof(int));
printf("float sizeof = %d \n",sizeof(float));
printf("double sizeof = %d \n",sizeof(double));
return 0;
}
Day 4 练习:输入输出
demo1.c — getchar 读取字符
#include <stdio.h>
int main() {
int ret1 = getchar();
int ret2 = getchar();
int ret3 = getchar();
putchar(ret1);
putchar(ret2);
putchar(ret3);
return 0;
}
demo2.c — putchar 输出字符
#include <stdio.h>
int main() {
int c = 'x';
putchar(c);
int c2 = 'y';
putchar(c2);
return 0;
}
demo3.c — getchar + putchar
#include <stdio.h>
int main() {
int ret = getchar();
putchar(ret);
return 0;
}
demo4.c — scanf 格式化输入
#include <stdio.h>
int main() {
char c = 0;
scanf("%c", &c); // 等价 c = getchar()
putchar(c);
int a = 0;
scanf("%d", &a);
printf("a = %d\n", a);
return 0;
}
demo5.c — scanf 多值输入
#include <stdio.h>
int main() {
int a = 0, b = 0, c = 0;
printf("请输入三个整数\n");
int ret = scanf("%d%d%d", &a, &b, &c);
printf("ret = %d, a=%d, b=%d, c=%d\n", ret, a, b, c);
return 0;
}
lianxi1.c — getchar 连续读取
#include <stdio.h>
int main() {
int c1 = getchar();
int c2 = getchar();
int c3 = getchar();
int c4 = getchar();
putchar(c1); // z
putchar(c2); // h
putchar(c3); // a
putchar(c4); // n
return 0;
}
lianxi2.c — 大小写转换
#include <stdio.h>
int main() {
int c = 0;
c = getchar();
if (c >= 'a' && c <= 'z') {
putchar(c - 32); // 小写转大写
} else if (c >= 'A' && c <= 'Z') {
putchar(c + 32); // 大写转小写
} else {
printf("不是字母\n");
}
return 0;
}