XYCOVO CONSOLE KURISU · WEB 渗透 / AI 辅助攻防 ONLINE
#042 · 2023-09-25✓ PASS

C 语言笔记

学习C语言时整理的知识点,从环境搭建、运算符、流程控制到指针和结构体。
c语言linux学习笔记AUTHOR: KURISU

学习 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为假

整数类型

类型字节范围(有符号)
char1-128 ~ 127
short2-32768 ~ 32767
int4-2^31 ~ 2^31-1
long4/8取决于平台
long long8-2^63 ~ 2^63-1

int 是测试机器位数的标准类型。在 16 位机器上 int == short,32 位机器上 long == int

实数类型(浮点数)

类型字节精度
float46位有效数字
double815位有效数字

浮点数不能直接用 == 与 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,后使用

逻辑运算符

&&   ||   !    // 与、或、非

逻辑与 &&:双目运算符,类似算术乘法

结果
111
100
010
000

短路原则:a && b && c && d 中只要有一个为假,后续不再判断。

逻辑或 ||:类似算术加法

结果
111
101
011
000

短路原则:只要有一个为真,后续不再判断。

逻辑非 !:单目运算符,取反

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

结果
000
011
101
110

移位 >> <<

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 == s20
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;
}
← #041 Linux 基本命令…#043 虚拟机安装 Ubuntu… →