基础部分

布尔类型

在C89的时候,没有特意的定义布尔这个类型,只能使用宏定义的方式,但是0表示假,除0外都表示为真。所以宏定义使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define BOOL int
#define TRUE 1
#define FALSE 0

int main(){
BOOL flag = TRUE;
BOOL fake = FALSE;

if(flag){
printf("我是flag,我是真值");
}

return 0;
}

在C99中,增加了布尔类型 _Bool,但是这个类型的值其实还是整数类型的别名,还是使用0代表false,使用1代表true,其他的非零数值都会被存储为1,所以 _Bool类型也是一种整数类型。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(){
_Bool flag = 1;
_Bool fake = 0;

if(flag){
printf("我是flag,我是真值");
}

return 0;
}

此外,C99还增加了一个头文件<stdbool.h>,定义了bool代表_Bool,并且定义为true为1,false为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdbool.h>

int main(){
bool flag = true;
bool fake = false;

if(flag){
printf("我是flag,我是真值");
}

return 0;
}

变量间的运算规则

大部分数据类型之间的运算遵循计算结果为数据类型占用较大的那个,除了char和short进行运算,结果转换为int类型。(自动类型转换)。宽类型赋值给窄类型,会将多余的长度截去(截断,不是四舍五入)。此时编译器会报警告,但是不会报错。

为了避免上述的警告,可以使用强制类型转换,需要使用强转符(),强制转换可能会有数据丢失。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(){
double d1 = 12.5;
int i1 = (int)d1;

return 0;
}

常量

常量的分类

字面常量

#define 定义的标识符常量

const修饰的常量

枚举常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// Created by YuHong on 2024/3/11.
//
#include <stdio.h>

#define X 1000

enum Sex {
MALE,
FEMALE,
SECRET
};

int main() {
// 1.字面常量
3.14;
1000;
// 2.#define
printf("%d\n", X);
// 3.C99新声明的方式:const
const int i1 = 200;
// 4.定义枚举常量
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
}

sizeof运算符

sizeof()用于判断数据类型或变量所占内存空间的大小,单位是字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
// 返回int类型占用字节的大小
int capacity1 = sizeof(int);
printf("%d\n", capacity1);

// 当返回值类型不确定的时候可以使用提供的size_t类型进行接受
// 测试sizeof返回值类型,size_t有专门的占位符:%zd或%zu。
size_t cap = sizeof(int);
printf("%zd\n", cap);
}

循环

goto关键字的使用

执行goto语句会无条件的跳转到制定的代码位置,搭配标签一起使用。

举例,无限打印adc:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
loop:
printf("adc\n");
goto loop;
return 0;
}

还有一个主要的用法是跳出多层循环。

数组

数组的定义方式

数据类型 数组名 [数组大小]

如:int arr[10],即定义了一个长度为10的int型数组。

数组的长度

因为在遍历数组的时候,使用循环会用到数组的长度作为参数,所以需要事先计算好数组的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
int arr[10];
size_t length = sizeof(arr) / sizeof(int);
// 为每一个数组元素赋值
for (int i = 0; i < length; i++) {
arr[i] = i;
}

// 遍历数组
for (int j = 0; j < length; j++) {
printf("%d\n", arr[j]);
}

return 0;
}

数组的其他定义方式

1
2
3
4
5
6
7
8
9
10
11
12
//1.数组可以在声明时,使用大括号,为每一个数组元素赋值
int arr[5]={1,2,3,4,5};
//2.允许在赋值的时候省略掉中括号中的个数,大括号中元素的数量是多少,长度就是多少
int arr[]={1,2,3,4,5};
//3.如果大括号中初始化的元素的数量小于长度,则剩余元素赋值为0
int arr[5]={1,2,3} == {1,2,3,0,0}
//4.数组初始化时,可以为指定位置的元素进行赋值,其余位置皆赋值为0
int arr[100]={[2]=10,[5]=30}
//5.指定位置的赋值和顺序赋值可以结合使用,表示为第1位是0,第6位是100,第七位是11,第11位是23
int arr[100]={0,[5]=100,11,[10]=23}
//6.省略成员数量,同时使用指定位置的赋值,数组的长度就是指定位置+1,此时下方数组的长度是9
int arr[]={[3]=4,[8]=10}

使用大括号赋值的时候必须在数组声明的时候执行,如果数组声明完毕之后,再进行大括号赋值会报错,

变长数组

数组声明的时候,数组长度除了使用常量,也可以使用变量或者表达式来指定数组的大小。这叫做变长数组(Variable-length Array,简称VLA)。

变长数组的根本特征是数组长度只有在运行时才能确定。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。如下方定义的数组都是变长数组。

1
2
3
4
int i =10;
int a1[i];
int a2[i+5];
int a3[i+k];

[!CAUTION]

注意:变长数组,在C99标准中引入,在C11标准中被标记为可选特性。某些编译器可能不支持变长数组,或者可能有特定的限制和行为。

如果你的编译器不支持变长数组,还可以考虑使用动态内存分配(使用malloc函数)来创建动态大小的数组。

1
2
3
4
5
6
//分配
int length = 5;
int *arr = (int*)malloc(length * sizeof(int)); //参数是字节数,所以就是长度*数据类型所占字节数

//释放
free(arr);

[!IMPORTANT]

使用完之后一定要及时回收内存资源。否则,会导致内存泄漏。

char型数组

1
char str[]={'a','b','c','d'};

字符串

C语言没有专门用于存储字符串的变量类型,字符串都被存储在char型数组中。在字符串结尾,会自动添加一个’\0’的转义字符作为字符串结束的标志。

1
2
3
4
5
6
7
8
//显式声明方式,标准写法
char str[] = {'h','e','l','l','o',',','w','o','r','l','d','\0'};
//简化写法,双引号中的字符串,不需要自己加'\0',C语言会自动添加
char str[]={"hello world"};
//进一步简化
char str[] = "hello world";
//如果一个字符串声明方式如下,由于必须留一个位置给'\0',所以只能容纳9个字符。
char str[10];

计算相关长度:

1
2
3
4
5
6
char str1[]={"hello world"};
char str2[] = "hello world";
//计算char型数组的长度
sizeof(str1)/sizeof(char); //长度是12
//计算字符串的长度
strlen(str2) //长度是11,因为字符串本身内容是不包含'\0'的,所以会比数组的长度要小1。

二维数组

定义赋值方式:

  1. 使用双层循环赋值。
  2. 使用大括号嵌套的方式赋值。
  3. 给特定位置的元素赋值。
  4. 使用一个大括号进行赋值,按顺序一层循环结束接着下一层。
1
int arr[][4]={1,2,3,4,5,6,7} //简化写法,7的后面还有一位,未赋值则为0。

提升部分

指针

指针的理解与定义

访问内存中变量存储的数据有两种方式,一种是直接访问,另外一种是间接访问。直接访问就是使用变量名进行的访问;间接访问就是采用指针进行访问。

通过地址能找到所需的变量单元,可以说地址指向该变量单元,将地址形象化称为指针。即,

  1. 变量:命名的内存空间,用于存放各种类型的数据。
  2. 变量名:变量名是给内存空间取得一个容易记忆的名字。
  3. 变量值:在变量单元中存放的数据值。
  4. 变量的地址:变量所使用内存空间的地址,即指针。
  5. 指针变量:一个变量专门用来存放另一变量在内存中的地址(即指针),则称他为指针变量。我们可以通过指针变量达到访问内存中另一个变量数据的目的。

指针变量的定义

数据类型 *指针变量名 [=初始地址值];

数据类型是指针变量所指向变量数据类型,可以是int、float、char等基本类型,也可以是数组等构造类型。

字符 * 用于告知系统这里定义的是一个指针变量,比如char *表示一个指向字符的指针。

1
2
3
//注意,指针变量的名字是p,不是*p。
int *p;
//指针变量中只能存放内存地址,不要将一个整数(或任何其他非地址类型的数据)赋给一个指针变量。

指针的运算

取地址运算符:&

取地址运算符,使用“&”符号来表示,作用:取出指定变量在内存中的地址,语法格式如下:

1
&变量

不要在给指针赋值之前使用此变量,此时的指针会随机指向一个内存地址,称为野指针

可以在调用前先将其置空:

1
2
int *p;
p = NULL;

取值运算符

取值运算符,使用“*”符号来表示,根据一个给定的内存地址取出对应其中存储的数据,语法格式如下:

1
*指针表达式

为了区分二者之间的差别,以及定义时的*,做一下代码对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main() {
int *p;

int num = 10;
printf("%d\n", num); //10
printf("%p\n", &num); //00000092d1bffda4
printf("%d\n", *&num); //10

p = &num;
printf("%p\n", p); //00000092d1bffda4
printf("%d\n", *p); //10

//通过指针变量可以修改内存地址位置上的值
*p = 20;
printf("%d", num); //20

return 0;
}

指针的常用运算

指针与整数值的加减运算

语法格式:指针±整数

指针与整数值的加减运算,表示指针所指向内存地址的移动(加:向后移动,减:向前移动)。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。

通过此操作,可以快速定位到要的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
short *s;
s = (short *) 0x1234;
//%hx表示十六进制short int类型,一个单位的short类型是2个字节,所以+1就是地址向后移动2个字节
printf("%hx\n", s); //0x1234
printf("%hx\n", s + 1); //0x1236
printf("%hx\n", s + 2); //0x1238

return 0;
}

只有指向连续的同类型数据区域,指针加、减整数才有意义。

举例:对于数组,arr[0]就是p,arr[1]就是 * (p+1),同理,arr[i]就是(p+i)。

同类指针相减运算

相同类型的指针允许进行减法运算,返回他们之间的距离,即相隔多少个数据单位(注意,非字节数)。高位地址减去低位地址,返回的是正值,反之则是负值。

返回的值的类型属于ptrdiff_t类型,这是一个带符号的整数类型的别名,具体类型根据系统不同而不同。这个类型的定义在头文件stddef.h文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
short *p1;
short *p2;

p1 = (short *) 0x1234;
p2 = (short *) 0x1236;

ptrdiff_t dest = p1 - p2;
printf("%d\n", dest); //-1,因为相差两个字节,正好是一个short类型占用的空间,所以是1,又因为是低位减高位所以是负数

return 0;
}

两个指针之间的加法是非法的,因为得出的结果没有任何意义。

同类指针的比较运算

例如,==、!=、<、<=、>、>=。比较的是各个内存地址的大小,返回值是整数1(true)或0(false)。

野指针

就是指,指针指向的位置是不可知的(随机性、不正确、没有明确限制的)。

野指针的成因

  1. 指针变量在定义时如果未进行初始化,其值是随机的,此时操作指针就是去访问一个不确定的地址,所以结果是不可知的,此时p就成为野指针。
  2. 指针越界访问数组。

野指针的避免

  1. 定义指针的时候,如果没有确切的地址赋值,则为变量赋值一个NULL是一个较好的编程习惯。
  2. 小心指针越界。
  3. 避免返回局部变量的地址。(局部变量的地址只在局部有效)
  4. 指针指向空间释放,及时置NULL。
  5. 指针使用之前,检查有效性。

二级指针(多重指针)

简单来说,一个指针变量的值是另外一个指针变量的地址。通俗来讲二级指针就是指向指针的指针。

格式:

1
数据类型 **指针名;

可以看出下方的二级指针

1
2
3
4
5
6
// 二级指针
short **pp1;
pp1 = &p1;
printf("%p\n", p1); //0000000000001234
printf(("%p\n"), pp1); //000000badc1ff810
printf("%p\n", &p1); //000000badc1ff810

指针与数组

带下标的指针

1
2
3
4
int arr[5];

int *p;
p = &a[0];

则,p[2] == *(p+2) == a[2]。
如果p最开始不是指向a[0],而是a[2],则p[2]就是a[4]。

&数组名

对应关系如图

数组的地址就是数组第一个元素的地址。所以他们两个的地址值是相同的。

但是arr+1的地址则是a[1]的地址。&(a+1),则是将数组作为一个长度单位,相当于数组结束的地址,所以二者结果不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
int arr[5] = {1, 2, 3, 4, 5};

printf("%p\n", arr); //00000019cafffbd0
printf("%p\n", &arr); //00000019cafffbd0

printf("%p\n", arr + 1); //00000019cafffbd4
printf("%p\n", &arr + 1); //00000019cafffbe4

return 0;
}

指针数组

数组指针与指针数组

数组指针:当指针变量存放一个数组的首地址时,此指针变量称为指向数组的指针变量,简称数组指针,本质是指针。

指针数组:用来存放指针的数组称为指针数组,本质是数组。

指针数组的使用

举例:

1
int *arr[5];

arr是一个数组,里面有5个元素,每个元素都是一个整形指针。

指向固定长度数组的指针变量

格式:(*标识符)[一维数组元素个数]

1
int (*p)[4];

由于p指向的是一个4个整形元素的数组,因此p+1的地址值就是加上4*4,即指向下一个一维数组。

函数

将特定的功能代码封装成函数的好处:实现代码复用,减少冗余,简化代码。

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

//无参函数声明
void func1();
//有参函数声明,参数只需要表明类型就可以
int add(int, int);

int main(){
func1();
add(12,10);

return 0;
}

void func1(){

}

int add(int i, int j){
return i + j;
}

参数传递机制

值传递

将实参值复制给形参,修改形参的值,不会影响外部实参的值(单向传递)。

对应的数据类型:基本数据类型、结构体、共用体、枚举类型。

地址传递之指针

将实参的地址传给形参,二者地址值相同。所以形参改变值实参也会随着改变(双向传递)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void swap(int *, int *);

int main() {
int a = 10;
int b = 20;
printf("%d,%d\n", a, b);
//将a和b的地址传入
swap(&a, &b);
printf("%d,%d\n", a, b);
}

void swap(int *a, int *b) {
const int temp = *a;
*a = *b;
*b = temp;
}

地址传递之数组

1
2
3
4
5
6
void swap(int arr[],int length);

int main(){
int arr[3]={1,2,3};
swap(arr,3);
}

可变参数的函数

有一些函数的参数数量是不确定的,此时可以使用C语言提供的可变参数函数,声明可变参数函数的时候,使用省略号表示可变数量的参数。

1
2
3
#include <stdarg.h>

int printf(const char* format, ...);

这里的…就表示传递任意数量的参数,但是它们必须都要和format字符串中的格式化相匹配。

[!CAUTION]

注意,…符号必须在参数序列的末尾,否则会报错。

可变参数的使用

  1. 为了使用可变参数,需要引入<stdarg.h>头文件。
  2. 在函数中,需要声明一个va_list类型的变量来存储可变参数。它必须在操作可变参数时,首先使用。
  3. 使用va_start函数来初始化va_list变量。它接受两个参数,参数1是可变参数对象,参数2是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。
  4. 使用va_arg函数来逐个获取可变参数的值。每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,参数1是可变参数对象,参数2是当前可变参数的类型。
  5. 使用va_end函数来结束可变参数的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdarg.h>

// 声明一个可变参数的函数,用于计算多个整数的平均值
double average(int count, ...) {
//count来记录可变参数的个数
va_list args; //args用来存放可变参数
va_start(args, count); //初始化va_list变量

double sum = 0.0; //记录所有整数的和
for (int i = 0; i < count; i++) {
//依次取出可变参数
sum += va_arg(args, int);
}

va_end(args); //结束可变参数的处理
return sum / count;
}

int main() {
double avg = average(5, 10, 20, 30, 40, 50);
printf("%.2lf", avg);
return 0;
}

指针函数的使用

函数的返回值类型是指针的函数称为指针函数。即返回的是一个地址,多数情况下,在返回字符串以及数组的时候使用此函数。

注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <malloc.h>
//如下的返回地址方法是错误的!因为局部变量随着函数的执行结束,就弹出栈了,所以返回的地址是错误的。
int *func(){
int m = 10;
return &m;
}

//上方返回局部变量地址的正确做法
int *func1(){
static int m = 10; //此时的m是静态变量,存储在静态数据区,不会随着函数的执行而结束。会随着main函数执行完毕销毁。
return &m;
}

//使用malloc实现返回局部变量,malloc函数开辟出来的空间是在堆空间中,不会随着函数的执行完毕而销毁。
int *func2(){
int *m = (int *)malloc(sizeof(int));
if(*m != NULL){ //只要内存分配成功,就给变量赋值
*m = 10;
}

return m;
}

函数指针的使用

一个函数本身就是一段内存中的代码,总是占用一段连续的内存区域。这段内存区域也有首地址,把函数的这个首地址(入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针

格式:

1
返回值类型 (*指针变量名)(参数列表)

实际使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void print(int m) {
printf("%d\n", m);
}

int main() {
// 声明一个函数指针
void (*print_pointer)(int);
// 赋值操作
print_pointer = &print;

// 方式1,使用函数名调用
print(20);
// 方式2,使用函数指针
(*print_pointer)(23);

return 0;
}

回调函数

只想函数a的指针变量的一个重要用途就把函数a的入口地址作为参数传递到其他函数b当中,此时的函数b就称为回调函数

1
2
3
4
5
void fun(int(*x1)(int),int(*x2)(int,int)){  //定义fun函数,形参是指向函数的指针变量
int a, b, ,i = 3, j = 5;
a = (*x1)(i); //调用x1函数,i是实参
b = (*x2)(i, j); //调用x2函数,i,j是实参
}

实际使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void print(int m) {
printf("%d\n", m);
}

int main() {
// 声明一个函数指针
void (*print_pointer)(int);
// 赋值操作
print_pointer = &print;

// 方式1,使用函数名调用
print(20);
// 方式2,使用函数指针
(*print_pointer)(23);

return 0;
}

函数说明符

C语言提供了一些函数说明符,让函数用法更加准确。

函数一旦定义,就可以被其他函数调用。但是当一个源程序由多个源文件组成时,在一个源文件中定义的函数能否被其他源文件中的函数调用呢?因此C语言又将函数分为两大类——内部函数和外部函数

内部函数(静态函数)

如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数被称为内部函数,此时,内部函数需要使用static修饰。

定义内部函数的格式:

1
static 类型说明符 函数名(<形参表>)

举例:

1
2
3
static int f(int a, int b){
...
}

外部函数

外部函数在整个源程序中都有效,只要定义函数时,在前面加上extern关键字即可。

定义外部函数的格式:

1
extern 类型说明符 函数名(<形参表>)

举例:

1
2
3
extern int f(int a, int b){
...
}

一些其他的变量修饰符

寄存器变量(register变量)

如果有一些变量使用非常频繁,则将其存放在寄存器中,CPU对寄存器的存取速度远大于对内存的存取速度。故可以提高运算效率。

语法格式:

1
register int f;

但是现在计算机的运行速度越来越快,故使用寄存器变量的必要性不大。

extern修饰变量

const修饰常量

结构体和共用体

结构体类型的基本使用

为什么需要结构体

C语言内置的数据类型,除了几种原始的基本类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用场景受限。

使用结构体,可以在内部定义多个不同数据类型的变量作为其成员。

结构体的理解

C语言提供了struct关键字,允许自定义复合数据类型,将不同数据类型的值组合在一起,这种类型称为结构体(structure)类型。

C语言没有其他语言的对象(object)和类(class)的概念,struct概念很大程度上提供了对象和类的功能。

声明结构体

1
2
3
4
5
struct 结构体名{
数据类型1 成员1;
数据类型2 成员2;
...
}; //注意最后有一个分号

举例:学生

1
2
3
4
5
6
struct Student{
int id;
char name[20];
char gender;
char address[50];
}

结构体的具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <string.h>

// 1.声明结构体,没有显式赋值的成员会被赋值为默认值
struct Student {
int id;
char name[30];
char gender;
int age;
char address[50];
};

// 对应格式4,cat1和cat2是同时声明的结构体变量
struct Cat{
char *name;
char gender;
} cat1, cat2;

// 对应格式5:不指定类型名而直接定义结构体类型变量
struct{ // 匿名结构体,只有t1和t2,不需要其他结构体变量,则可以省去结构体类型名
char *name;
char gender;
} t1, t2;

// 对应的格式6:使用typedef命令
typedef struct employee{
char *name;
int age;
}Emp;

int main() {
// 2.声明结构体变量
struct Student stu1;

// 赋值格式1
stu1.id = 1001;
// stu1.name = "小红"; 这种方式赋值会报错,针对于数组,不能重新赋值
strcpy(stu1.name, "小红");
stu1.gender = '男';
stu1.age = 24;
strcpy(stu1.address, "北京");

// 赋值格式2:struct 结构体名 结构体变量 = {初始化数据}
struct Student stu1 = {"小红", '男', 24, "北京"};

// 赋值格式3:struct 结构体名 结构体变量 = {.成员1 = xxx,.成员2 = xxx}
struct Student stu1 = {.gender = '男', .age = 24, .address = "北京"};

//赋值格式4:声明类型的同时定义变量
cat1.gender = 'M';
cat2.gender = 'F';
return 0;

// 赋值格式5
t1.gender = 'M';

// 赋值格式6,相当于起了别名,声明新变量的时候更简便
Emp e1;
}

结构体嵌套

结构体的成员也是变量,那么成员可以是基本数据类型,也可以是数组、指针、结构体等类型。如果结构体的成员是另一个结构体,这就构成了结构体的嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <string.h>

struct Name {
char firstName[50];
char lastName[50];
};

struct Student {
int age;
struct Name name;
char gender;
} stu1;

int main() {
stu1.age = 52;
stu1.gender = 'M';
strcpy(stu1.name.firstName, "Tom");
strcpy(stu1.name.lastName, "Smith");

printf("gender=%c,age=%d,name=[%s %s]", stu1.gender, stu1.age, stu1.name.firstName, stu1.name.lastName);

return 0;
}

单链表与二叉树的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 单链表的节点定义

#include <stdio.h>

struct Node {
int data; //节点中保存的数据
struct Node *next; //节点指针,指向下一个节点元素
};

// 另一种声明格式
typedef struct Node1 {
int data;
struct Node1 *next;
} LNode;

// 二叉树结构的节点定义如下
typedef struct BTNode {
int data;
struct BTNode *lchiid; //对应左子树
struct BTNode &rchild; //对应右子树
} BTNode;

int main() {
// 测试单链表节点
struct Node node1 = {.data = 10};
struct Node node2 = {.data = 20, .next = &node1};

LNode node3 = {.data = 30};
LNode node4 = {.data = 40, .next = &node3};

return 0;
}

结构体占用的空间大小

结构体占用的内存空间不是元素占用空间之和,为了计算效率,都必须是int类型存储空间的整数倍。如果int类型的存储是4字节,那么struct类型的占用空间就是4的整数倍。

1
2
3
4
5
// 下方共占用4*2=8个字节,每多一个元素就加4
struct A{
char a;
int b;
}

结构体变量赋值的操作

是值传递,不是地址传递,没给一个新的变量赋值就会在内存开辟一块新的空间。

其中的元素使用指针(字符串)的话还是地址传递。

结构体数组

数组元素是结构体变量构成的数组。先定义结构体类型,然后用结构体类型定义数组变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

struct Person {
char name[20];
int age;
};

// 声明的时候同时赋值
struct Student {
char *name;
int age;
} stus1[2] = {{"Tom", 12}, {"Jerry", 23}};

int main() {
// 1.如何创建结构体数组
struct Person person[3];
// 2.如何给结构体数组的元素赋值
// 声明的同时进行赋值,数组的长度可以省略
struct Person person1[3] = {
{.name = "as", .age = 52}, {.name = "xiaojie", .age = 20}, {.name = "asdas", .age = 62}
};
// 可以省略.name属性名
struct Person person2[] = {
{"as", 52}, {"xiaojie", 20}, {"asdas", 62}
};
// 3.如何调用数组元素内部成员,结构体数组名[下标].成员名
printf("%s\n", stus1[0].name);
}

结构体指针

指向结构体变量的指针(将结构体变量的起始地址存放在指针变量中)

具体应用场景:可以指向单一的结构体变量;可以用作函数的参数;可以指向结构体数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

struct Person {
char name[20];
int age;
};

int main() {
struct Person person = {"haha", 20};
struct Person *ptr_person = &person;

printf("%s\n", (*ptr_person).name);
// 也可以使用下面这种箭头进行调用
printf("%d\n", ptr_person->age);

return 0;
}

结构体传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

struct Person {
char *name;
int age;
char *address;
};

void addAge(struct Person per) {
per.age += 1;
}

void addAge1(struct Person *per) {
// 等价
// (*per).age = (*per).age + 1;
per->age = per->age + 1;
}

int main() {
struct Person person = {"ad", 20, "Asdda"};
addAge(person);
printf("%d\n", person.age);

addAge1(&person);
printf("%d\n", person.age);

return 0;
}

->操作符

1
2
// (*per).age = (*per).age + 1;
per->age = per->age + 1;

共用体类型(union)

C语言提供了共用体类型(union),用来自定义可以灵活变更的数据结构。它内部可以包含各种属性,但同一时间只能有一个属性,因为所有属性都保存同一个内存地址,后面写入的属性会覆盖前面的属性。这样做最大的好处就是节省内存空间。

共用体变量所占的内存长度等于最长的成员的长度;几个成员共用一个内存区。

共用体也支持箭头操作符->

声明共用体

1
2
3
4
5
union 共用体类型名称{
数据类型 成员名1;
数据类型 成员名2;
数据类型 成员名3;
};

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

union Data {
short a;
float b;
char c;
};

// 声明格式2
union Data1 {
short a;
float b;
char c;
} data2;

int main() {
// 声明共用体变量格式1
union Data data1;

// 调用共用体变量的成员
data1.a = 12;

// 声明变量的时候赋值
union Data data4 = {.a = 12};

union Data data5 = {12}; //默认给首成员赋值
return 0;
}

C语言的常用函数

字符串声明的两种方式及对比

1
2
3
4
5
int main() {
char s1[] = "hello";
char *s2 = "hello";
}

两种方式的区别:

  1. 指针指向的字符串,在C语言内部被当做常量,不能修改字符串本身。如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。
  2. 指针变量可以指向其他字符串,但是字符数组变量不能指向另一个字符串,只能使用strcpy()实现。

字符串常用函数

在程序开头引入库文件#include <string.h>。

  1. strlen(),返回字符串的字节长度,不包含末尾的’\0’。
  2. strcpy(字符数组1, 字符数组2),字符串的复制,将字符数组2复制到字符数组1中。
  3. strncpy(str1, str2, n),将字符串2中前面n个字符复制到字符数组1中去。
  4. strcat(字符数组1, 字符数组2),把两个字符数组中的字符串连接起来,字符数组1必须足够大容纳两个字符串的大小。
  5. strncat(str1, str2, n),将字符串2中前面n个字符连接到字符数组1中去。
  6. strcmp(字符串1, 字符串2),比较字符串1和字符串2,将两个字符串自左向右逐个字符相比(ASCII),直到出现不同的字符或遇到’\0’为止。如字符全部相同,则返回0,如果返回正数则字符串1大,反之字符串2大。
  7. strlwr(),将字符串中的大写字母转换为小写;strupr(),将字符串中的小写字母转换为大写。

基本数据类型和字符串的转换

基本数据类型->字符串

sprintf()函数可以将其他数据类型转换为字符串类型。此函数声明在stdio.h文件中。

字符串->基本数据类型

调用头文件中的stdlib.h中的atoi(),整型;或atof(),浮点型即可。

char类型的不可以用上述两个函数,用数组下标的方式取值即可。

日期和时间相关的函数

相关的头文件是time.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <time.h>

void test() {
int i = 0;
int sum = 0;
int j = 0;
for (i = 0; i < 100000000; i++) {
sum = 0;
for (j = 0; j < 100; j++) {
sum += j;
}
}
}

int main() {
printf("程序启动...\n");

time_t start_t;
// 先得到执行test之前的时间
time(&start_t); //获取当前时间
test(); //执行test()
time_t end_t;
time(&end_t);

//存放时间差
double diff_t = end_t - start_t;
printf("时间差是%lf\n", diff_t); //时间差是7.000000

// 获取时间对应的字符串的表示

char *startTimeStr = ctime(&start_t);
printf("%s\n", startTimeStr); //Tue Mar 19 11:54:35 2024

return 0;
}

数学运算相关的函数

首先引入math.h头文件、

  1. double exp(double x):返回e的x次幂的值
  2. double log(double x):返回x的自然对数(基数为e的对数)
  3. double pow(double x, double y):返回x的y次幂
  4. double sqrt(double x):返回x的平方根
  5. double fabs(double x):返回x的绝对值

void指针

void指针只有内存块的地址信息,没有类型信息。void指针与其他数据类型指针是互相转换关系,任意类型的指针都可以转换为void指针,反之void指针也可以转换为其他数据类型的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
int x = 10;
int *p = &x;
printf("%d\n", *p);

void *q = &x;

// 无类型指针变量可以与其他类型的指针变量相互转换
int *r = q;
q = p;

return 0;
}

内存动态分配函数

头文件stdlib.h声明了四个关于内存动态分配的函数。所谓动态分配内存,就是按需分配,申请才能获得。

malloc()和free()

函数原型:

1
void *malloc(unsigned int size);

作用:在内存的动态存储区(堆区)中分配一个长度为size连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。

由于返回值类型是void,应通过显式类型转换后才能存入其他类型的指针变量。如果分配不成功,返回空指针(NULL)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

// 声明结构体
struct Node {
int data;
struct Node *next;
};

int main() {
// 给int类型的变量分配空间
int *ptr_i = (int *) malloc(sizeof(int));

*ptr_i = 10;
printf("%d\n", *ptr_i);

// 给int型的数组分配空间
int arrLen = 10;
int *ptr_arr = (int *) malloc(sizeof(int) * arrLen);
for (int i = 0; i < arrLen; i++) {
ptr_arr[i] = i;
}

// 给结构体变量分配空间
struct Node *ptr_node = (struct Node *) malloc(sizeof(struct Node));

// 关于NULL值的判断
if (ptr_node != NULL) {
// 意味着分配成功


// 释放内存空间
// 如果在当前函数结束之后没有释放相关内存,则此内存空间将不会被释放,存在内存泄露
free(ptr_node);
free(ptr_arr);
free(ptr_i);
} else {
// 分配失败
}
return 0;
}

calloc()

函数原型:

1
void *calloc(unsigned int n, unsigned int size);

作用:在内存的动态存储区(堆区)分配n个,单位长度为size的连续空间,这个空间一般比较大,总共占用n*size个字节。并将该内存空间的首地址作为函数的返回值。如果函数没有成功执行,返回NULL。

calloc()函数适合为一维数组开辟动态存储空间,n为数组的个数,每个元素长度为size。

realloc()

函数原型:

1
void *realloc(void *p, unsigned int size);

作用:重新分配malloc()或calloc()函数获得的动态空间的大小,即调整动态内存空间的大小,将先前开辟的内存块的指针p指向动态空间大小改变为size,单位字节。返回值是一个全新的地址(数据也会自动复制过去),也可能返回和原来一样的地址。分配失败返回NULL。优先缩减原有内存块,所以一般返回的都是原地址;如果新内存块小于之前的内存块,则丢弃超出的部分。

文件操作

C程序中,对于文件中数据的输入\输出一“流”的方式进行,可以看做是数据的流动。

C标准I\O

scanf()和printf()

getchar()和putchar()

gets()和puts()

C文件的读写

创建/打开文件

使用fopen()函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型FILE的一个对象,类型FILE包含了所有用来控制流的必要的信息。

函数原型:

1
FILE *fopen(const char *filename e, const char * mode);

说明:在这里,filename是字符串,用来命名文件;访问模式mode的值可以是下列值中的一个。

关闭文件

使用完文件后(读/写),一定要将该文件关闭。

函数原型:

1
int fclose(FILE *fp);

如果成功关闭文件,fclose()返回返回零。此时,会清空缓存区数据,关闭文件,并释放用于该文件所有的内存空间。

如果关闭文件发生错误,函数返回EOF。EOF是一个定义在stdio.h中的常量。

写入文件

fputc()函数原型:

1
int fputc(int c, FILE *fp);

说明:函数把参数c的字符值写入到fp所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回EOF。

fputs()函数原型:

1
int fputs(const char *s, FILE *fp);

说明:函数fputs()把字符串s写入到fp所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回EOF。

fprintf()函数原型:

1
int fprintf(FILE *fp, const char *format, ...);

功能与fputs()类似,将一个字符串写入文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
// 1.声明文件的具体路径
char *filePath = "D:\\IO\\hello.txt";
// 2.创建/打开文件
FILE *file_ptr = fopen(filePath, "w");
// 3.将数据写出到文件中
// 使用fputs()
fputs("hello world", file_ptr);
// 使用fprintf()
fprintf(file_ptr, "132000");
// 4.关闭文件
fclose(file_ptr);
}

读取文件

fgetc()函数原型:

1
int fgetc(FILE *fp);

说明:函数从fp指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF。

fscanf()函数原型:

1
int fscanf(FILE *fp, const char *format, ...);

说明:使用fscanf()函数从文件中读取格式化的数据,比如整形、浮点型等各种类型的数据。format参数指定了数据的格式,后面的参数是用于存储读取数据的变量。

如果使用fscanf()从文件中读取字符串,会在遇到第一个空白字符(空格、制表符、换行符等)时,停止读取,之后的内容会被忽略。

fgets()函数原型:

1
char *fgets(char *buf, int n, FILE *fp);

说明:此函数按行读取数据,它从文件中读取一行数据,(包括换行符’\n’),并将这一行的内容存储到指定的缓存区中。参数buf是用于存储读取的文本的缓存区,n是缓存区大小,fp是文件指针。

如果成功读取,返回参数buf,即读到的字符串的首地址。如果达到文件末尾或读取失败,返回NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
// 1.声明文件的具体路径
char *filePath = "D:\\IO\\hello.txt";
// 2.打开文件
FILE *file_ptr = fopen(filePath, "r");
// 3.将数据读取到内存中
// 使用fscanf()
// char cBuffer[10];
// fscanf(file_ptr, "%s", cBuffer);
// printf("%s", cBuffer); //打印了hello,后面的五位字符没有打印,是因为遇到空格了,中断了读取。
// 使用fgets()
char cBuffer[100];

// 当不到末尾就一直执行,就会向下一行读取
while (fgets(cBuffer, 100, file_ptr) != NULL) {
printf("%s", cBuffer);
}
// 4.关闭文件
fclose(file_ptr);
}