5.1k 词

结构体

概念

结构体是一种构造类型,它是由若干不同类型的数据项组合而成的一个数据集合。

描述一组具有相同数据的有序集合,用于处理大量相同类型的数据运算,有时我们需要将不同类型的数据组合成一个有机的整体方便引用。

类似与面向对象编程中定义一个类以及其属性,不过C语言中不能在结构体中定义函数,不过可以使用函数指针实现类似于面向对象编程的方法来调用类的函数。

结构体类型和变量的定义

定义方法1:先定义结构体类型,再定义变量。

1
2
3
4
5
6
7
8
//定义类型
struct Student{
char name[20];
int age;
float score;
};
//定义变量
struct Student Bob,Lucy,...,Tom;

定义变量技巧:使用typedef在定义结构体类型的时候初始化一个新的类型名。

很多时候我们在创建结构体变量的时候常常需要使用定义方法1来创建变量,但是冗长的定义语句会降低代码的可读性,这时我们就可以用typedef来将一种结构体类型封装为一个变量类型,这样我们就可以像定义普通变量一样来定义结构体变量了。

1
2
3
4
5
6
7
8
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

//使用Stu来定义结构体变量
Stu Bob,Lucy,...,Tom;

定义方法2:在定义结构体类型的时候,顺便定义一些结构体变量。

如果这种结构体类型是全局类型,那么使用这种方法定义结构体变量也是全局变量了。

1
2
3
4
5
6
//定义类型
struct Student{
char name[20];
int age;
float score;
}Bob,Lucy,...,Tom;//定义变量

另外,使用这种方法还可定义一些无名结构体变量,无名结构体变量使用这种方法在定义类型后定义变量。
无名结构体类型无法使用第一种方法定义变量。
无名结构体定义无名结构体时必须定义该结构体类型的至少一个变量
无名结构体常用于避免相同类型的结构体的重复定义。

1
2
3
4
5
6
//定义无名结构体类型
struct{
char name[20];
int age;
float score;
}Bob,Lucy,...,Tom;//定义变量

结构体变量的操作

初始化

结构体初始化可以一次性初始化多个属性,但是要按照结构体类型的定义顺序来初始化变量。

1
2
3
4
5
6
7
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

Stu Bob = {"Bob",18,95.5};//定义结构体变量并且初始化

访问结构体变量

使用.运算符来访问结构体变量中的属性。
也可以使用结构体指针来访问结构体变量中的属性。

1
2
printf("%s",Bob.name);//使用结构体变量名来访问结构体变量中的属性
printf("%s",(*Bob).name);//使用结构体指针来访问结构体变量中的属性

结构体嵌套

结构体可以嵌套定义,也可以嵌套初始化。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

typedef struct Teacher{
char name[20];
Stu stu;
}Tea;

Tea tea = {"Teacher",{"Bob",18,95.5}};//定义结构体变量并且初始化

结构体数组

结构体可以定义数组,结构体数组可以用于存储一组相同类型的数据。

1
2
3
4
5
6
7
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

Stu stu[10];//定义结构体数组

结构体与指针

结构体指针

结构体指针可以指向结构体变量,也可以指向结构体数组。

1
2
3
4
5
6
7
8
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

Stu stu[10];//定义结构体数组
Stu *p = stu;//定义结构体指针并指向结构体数组

结构体指针访问结构体变量

使用->运算符来访问结构体指针指向的结构体变量中的属性。

1
printf("%s",p->name);//使用结构体指针来访问结构体变量中的属性

结构体指针作为函数参数

结构体指针可以作为函数的参数,这样就可以在函数内部修改结构体变量的值了。

1
2
3
4
5
6
7
8
9
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

void getStu(Stu *stu){
stu->age = 18;
}

结构体内指针成员

在定义结构体类型的时候,可以在结构体内部定义指针类型。方便引用。

1
2
3
4
5
6
typedef struct Student{
char name[20];
int age;
float score;
char *p;
}Stu;

使用结构体内成员的函数指针连接内部函数实现面向对象写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
typedef struct Student{
char name[20];
int age;
int id;
void (*stuIDF)(struct Student s);
}Stu;

void getStuName(struct Student s){
printf("%s\n",s.name);
}

void getStuID(struct Student s){
printf("%d\n",s.id);
}

int main(){
Stu stu = {"Bob",18,333111000,getStuName};
stu.stuIDF(stu);

stu.stuIDF = getStuID;
stu.stuIDF(stu);
return 0;
}

结构体与函数

结构体可以作为函数的参数,也可以作为函数的返回值。

1
2
3
4
5
6
7
8
9
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

Stu getStu(Stu stu){
return stu;
}

结构体与文件

结构体可以存储到文件中,也可以从文件中读取结构体数据。

1
2
3
4
5
6
7
8
9
10
typedef struct Student{
char name[20];
int age;
float score;
}Stu;

Stu stu;
FILE *fp = fopen("data.txt","wb");
fwrite(&stu,sizeof(Stu),1,fp);
fclose(fp);

结构体的内存空间

结构体变量大小是所有成员之和吗?

实际上,结构体变量大小是所有成员之和,但是还要考虑对齐问题。

结构体成员的地址必须相对于结构体首地址的偏移量是成员大小的整数倍。

确定内存的颗粒度

以多少个字节为单位(颗粒大小)开辟内存给结构体变量分配内存的时候,会去结构体变量中找基本类型的成员中基本类型的成员占字节数多,就以它大大小为颗粒单位开辟内存。而实际上不同的编译器关于结构体的内存分配有不同的规则,但大多数遵循以下的规律。

  1. 成员中只有char型数据,以1字节为单位开辟内存
  2. 成员中出现了short 类型数据,没有更大字节数的基本类型数据,以2字节为单位开辟内存
  3. 出现了int、float没有更大字节的基本类型数据的时候以4字节为单位开辟内存。
  4. 出现了double类型的数据,要根据编译器确定,VC编译器是8字节,gcc编译器是4字节。
  5. 出现了long long类型的数据,以8字节为单位开辟内存。
  6. 如果成员中出现了数组,分配空间时将数组视作多个成员的集合

结构体成员的内存对齐

既然已经确定了颗粒度,那么让结构体成员的内存在分配时对齐颗粒度(这下真是“对齐颗粒度”了)就可以提高访问速度,但是会浪费一些内存空间。

结构体成员对齐的规则是:结构体成员的地址相对于结构体首地址的偏移量是颗粒度大小的整数倍。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<stdlib.h>
struct temp{
char a;
int b;
short c;
}TempS;

int main(){

printf("Size of char:%d\n",sizeof(char));
printf("Size of int:%d\n",sizeof(int));
printf("Size of short:%d\n",sizeof(short));

printf("Size of Struct:%d\n",sizeof(TempS));
return 0;
}

这个结构体类型成员有charintshort三种类型,根据上述规则可以确定,这个结构体类型以最大的int类型的4字节为单位开辟内存,因此虽然结构体中有小于4字节的charshort类型,但是依然给他们分配了4字节的空间,所以这个结构体的大小是12字节。


共用体

概念

在进行某些算法的时候,需要使几种不同类型的变量存到同一段内存单元中,几个变量所使用空间相互重叠这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型,共用体内多个成员共享一片储存空间,共用体的大小由最大的成员决定

共用体和结构体用法相似,也是一种构造类型。

共用体的特点

  1. 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
  2. 共用体变量中起作用的成员是最后一次存放的成员,在赋值任意一个成员后所有成员的值会被覆盖
  3. 共用体变量的地址和它的各成员的地址都是同一地址

测试代码: [点击运行]

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

union data {
int i;
float f;
char str[20];
};

int main() {
union data data;
data.i = 10;
printf("data.i: %d\n", data.i);
printf("data.f: %f\n", data.f);
printf("data.str: %s\n", data.str);

data.f = 220.5;
printf("data.i: %d\n", data.i);
printf("data.f: %f\n", data.f);
printf("data.str: %s\n", data.str);
return 0;
}

输出结果:

output
1
2
3
4
5
6
7
data.i: 10
data.f: 0.000000
data.str:

data.i: 1130135552
data.f: 220.500000
data.str:

共用体的大小

共用体的大小由最大的成员决定,如果共用体中包含多个结构体,共用体的大小由结构体中最大的成员决定。

测试代码: [点击运行]

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

union data {
int i;
float f;
char str[20];
struct {
int a;
char b;
} s;
};

int main() {
printf("sizeof(union data): %lu\n", sizeof(union data));
return 0;
}

输出结果:

output
1
sizeof(union data): 20
3.1k 词

const 关键字

const 关键字用于定义常量,常量一旦被定义,其值就不能被修改。即变为只读状态,被const修饰的变量编译运行时存放在静态区。

有些编译器或者IDE可能不直接检查指针赋值时const的修饰符,所以可能会出现const修饰的局部变量可用指针修改的情况,但从C98到C23,都把这种操作视为非法的,因此如果你使用了指针去修改const变量,可能会产生因环境问题导致程序无法成功编译的问题。

const修饰变量

测试代码:**[点击运行]**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
const int a=100;
int main(int argc ,char *argv[]){
printf("a=%d\n", a);

a=666;
printf("a=%d\n", a);

int*p=&a;
*p = 888;
printf("a=%d\n", a);

return 0;
}

输出结果:

compilation
1
2
3
4
5
6
7
8
9
10
main.cpp:7:6: error: cannot assign to variable 'a' with const-qualified type 'const int'
a=666;
~^
main.cpp:3:11: note: variable 'a' declared const here
const int a=100;
~~~~~~~~~~^~~~~
main.cpp:10:9: error: cannot initialize a variable of type 'int *' with an rvalue of type 'const int *'
int*p=&a;
^ ~~
2 errors generated.

由以上测试代码可以知道:

1. const修饰的变量不能使用赋值语句修改
2. const修饰的变量不能赋值给非const指针

const修饰指针

const可以修饰变量,这当然包括指针;同时指针也可以指向const修饰的变量,关键字的顺序决定了指针和指针指向的内容是否被修饰符修饰。

静态变量的指针

即指向静态变量的指针,const关键字只修饰了指针指向的内容,而不是指针本身。
静态变量的指针只能指向静态变量,但是指针本身不是静态的,所以可以修改指针的值,但是不可以修改指针指向的变量的值。

写法:
const int* p;//推荐int const *p;//不推荐

测试代码:**[点击运行]**

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

int main (int argc ,char *argv[]){
int const *p;
const int* q;
const int a = 100;
const int b = 200;

p = &a;
q = &b;
printf("p=%d\n", *p);
printf("q=%d\n", *q);

p = &b;
q = &a;
printf("p=%d\n", *p);
printf("q=%d\n", *q);

return 0;
}

输出结果:

output
1
2
3
4
p=100
q=200
p=200
q=100

静态的指针变量

即静态的指针,const关键字只修饰了指针,而不是指针指向的变量。因此可以通过指针修改指针指向的变量的值,但是指针自己不能重新被赋值。

写法:
**int* const p;**可见定义指针的时候星号跟着类型走还是比较规范的。

测试代码:**[点击运行]**

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

int main (int argc ,char *argv[]){
int a = 100;
int b = 200;
int* const p = &a;

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

//尝试修改p指向的内容的值
*p = 666;
printf("p=%d\n", *p);

//尝试修改p的指向 //ERROR block
p = &b;
printf("p=%d\n", *p);//

return 0;
}

输出结果:

output
1
2
3
4
5
6
7
8
9
10
11
12
//不注释掉ERROR代码块的编译报错
main.cpp:16:7: error: cannot assign to variable 'p' with const-qualified type 'int *const'
p = &b;
~ ^
main.cpp:7:16: note: variable 'p' declared const here
int* const p = &a;
~~~~~~~~~~~^~~~~~
1 error generated.

//注释掉ERROR语句的输出:
p=100
p=666

静态变量的静态指针

指针和指针的类型都是静态的,指针本身不能修改,指针指向的内容也不能被修改

写法:
const int* const p;

测试代码: **[点击运行]**

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

int main (int argc ,char *argv[]){
const int a = 100;
const int b = 200;
const int* const p = &a;

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

//尝试修改p指向的内容的值//ERROR block
*p = 666;
printf("p=%d\n", *p);

//尝试修改p的指向 //ERROR block
p = &b;
printf("p=%d\n", *p);//

return 0;
}

输出结果:

output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//不注释掉ERROR代码块的编译报错
main.cpp:11:8: error: read-only variable is not assignable
*p = 666;
~~ ^
main.cpp:15:7: error: cannot assign to variable 'p' with const-qualified type 'const int *const'
p = &b;
~ ^
main.cpp:6:22: note: variable 'p' declared const here
const int* const p = &a;
~~~~~~~~~~~~~~~~~^~~~~~
2 errors generated.

//注释掉ERROR语句的输出:
p=100

总结

const修饰符可以让程序变得更健壮,const经常用在指针变量上,特别是动态申请空间的指针,使用const变量修饰后可以有效防止指针丢失从而造成内存泄漏。但是需要注意的是在定义指针时const修饰符的写法,
const修饰符的位置不同,指针指向的内容和指针本身是否可以被修改的含义也不同。所以应该使用更有规律的定义写法从而提高代码的可读性。

8k 词

字符串处理函数

目录


字符串长度函数

strlen

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
size_t strlen(const char *str);
//size_t本质是unsigned int的typdef

/*
功能:
测传入的字符串指针str的字符串中字符的个数,不包括字符串结束符'\0'
参数:
str:字符串指针
返回值:
返回字符串的长度,不包括字符串结束符'\0'。
*/

sizeof函数不同,strlen返回的是字符串的长度;sizeof函数返回的是字符串开辟空间的大小,如果是一般的字符串常量,则这个空间大小就是字符串长度加上字符串结束符\0的大小,如果是字符数组的字符串,那就是数组大小。

字符串复制函数

strcpy

1
2
3
4
5
6
7
8
9
10
11
#include <string.h>
char *strcpy(char *dest, const char *src);
/*
功能:
将字符串src复制到字符串dest中
参数:
dest:目标字符串指针
src:源字符串指针
返回值:
返回目标字符串指针dest
*/

strcpy复制的字符串包含源字符串的\0,而且遇到的第一个\0会让函数结束复制并返回

注意:
在使用字符串复制函数时,函数不会检查dest的内存空间是否大于src,因此要保证目标字符串dest必须有足够的空间来存放源字符串src,否则会发生内存越界。

多个\0的情况

如果src有多个\0strcpy只会复制第一个\0之前的内容。

测试代码:[点击运行]

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

int main() {
char dest[50] = "Hello\0World";
char src[20] = " everyone!";

strcpy(dest, src);

printf("%s\n", dest);
}

输出结果:

output
1
everyone!

strncpy

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
char *strncpy(char *dest, const char *src, size_t n);
/*
功能:
将字符串src的前n个字符复制到字符串dest中
参数:
dest:目标字符串指针
src:源字符串指针
n:要复制的字符个数
返回值:
返回目标字符串指针dest
*/

strncpy的返回条件不再是遇到\0,而是复制n个字符。n数完了就返回

注意:
如果n的大小大于src的大小,则在之后用\0填充dest
strncpy也要注意内存越界的问题。


字符串连接函数

strcat

1
2
3
4
5
6
7
8
9
10
11
#include <string.h>
char *strcat(char *dest, const char *src);
/*
功能:
将字符串src连接到字符串dest的末尾
参数:
dest:目标字符串指针
src:源字符串指针
返回值:
返回目标字符串指针dest
*/

strcat的追加位置是dest字符串的第一个\0前,如果dest字符串没有\0,则strcat会认为dest是一个尚未初始化的空间,则相当于直接给dest赋值src的内容。

注意:
strcat也要注意内存越界的问题

多个\0的情况

  1. 如果dest有多个\0strcat会覆盖掉第一个\0之后的内容。

    测试代码:[点击运行]

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

    int main() {
    char dest[50] = "Hello\0World";
    char src[20] = " everyone!";

    strcat(dest, src);

    printf("%s\n", dest);
    return 0;
    }

    输出结果:

    output
    1
    Hello everyone!
  2. 如果src有多个\0strcat只会追加src字符串第一个\0前的内容,而忽略src中后续的任何字符。
    测试代码:[点击运行]

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

    int main() {
    char dest[50] = "Hello ";
    char src[50] = "world\0again\0lastly";

    strcat(dest, src);

    printf("%s\n", dest);
    return 0;
    }

    输出结果:

    output
    1
    Hello world

strncat

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
char *strncat(char *dest, const char *src, size_t n);
/*
功能:
将字符串src的前n个字符连接到字符串dest的末尾
参数:
dest:目标字符串指针
src:源字符串指针
n:要连接的字符个数
返回值:
返回目标字符串指针dest
*/

注意:
如果n大于src字符串中字符的个数(遇到第一个\0为止作为字符个数),则只将src字符串中第一个\0前的内容追加到dest指向的字符串之后。

测试代码:[点击运行]

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

int main() {
char dest[50] = "Hello ";
char src[50] = "world\0again\0lastly";

strncat(dest, src, 11);//显然在"again"之后的位置

printf("%s\n", dest);
return 0;
}

输出结果:

output
1
Hello world

字符串比较函数

strcmp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <string.h>
int strcmp(const char *str1, const char *str2);
/*
功能:
比较字符串str1和str2的大小
参数:
str1:字符串1指针
str2:字符串2指针
返回值:
如果str1 < str2,返回负数
如果str1 > str2,返回正数
如果str1 == str2,返回0
*/

比较规则:

  1. 从两个字符串的第一个字符开始比较ASIIC码,如果相等,则比较下一个字符,直到遇到不相等的字符或者遇到\0为止。
  2. 如果两个字符串相等,则返回0。
  3. 如果str1字符串大于str2字符串,返回1
  4. 如果str1字符串小于str2字符串,返回-1

注意:
strcmp返回值为0反而是两个字符串相同,需要注意。

测试代码:[点击运行]

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

int main() {
char str1[50] = "Hello";
char str2[50] = "World";
int result = strcmp(str1, str2);

printf("strcmp result: %d\n", result);
return 0;
}

输出结果(具体值可能和编译器有关):

output
1
strcmp result: -15

strncmp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <string.h>
int strncmp(const char *str1, const char *str2, size_t n);
/*
功能:
比较字符串str1和str2的前n个字符的大小
参数:
str1:字符串1指针
str2:字符串2指针
n:要比较的字符个数
返回值:
如果str1 < str2,返回负数
如果str1 > str2,返回正数
如果str1 == str2,返回0
*/

注意:
如果n大于str1str2字符串中字符的个数(遇到第一个\0为止作为字符个数),则只比较到第一个\0为止。

测试代码:[点击运行]

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

int main() {
char str1[50] = "Hello";
char str2[50] = "Hello World";
int result = strncmp(str1, str2, 3);

printf("strncmp result: %d\n", result);
return 0;
}

输出结果:

output
1
strncmp result: 0

字符串查找函数

strchr

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
char *strchr(const char *str, int c);
/*
功能:
在字符串str中查找ASIIC码c对应的字符**第一次**出现的位置
参数:
str:字符串指针
c:要查找的字符(用int强转后传入)
返回值:
如果找到字符c,返回指向该字符的指针
如果未找到字符c,返回NULL
*/

测试代码:**[点击运行]**

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

int main() {
char str[50] = "Halo everyone!";
char *result = strchr(str, 'e');

//打印返回的指针会显示查找到的第一个字符之后的内容
if(*result != NULL){
printf("strchr: found at %d\n", result-str);
printf("strchr result: %s\n", result);
}
else{
printf("strchr: not found\n");
}
return 0;
}

输出结果:

output
1
2
strchr: found at 5
strchr result: everyone!

strrchr

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
char *strrchr(const char *str, int c);
/*
功能:
在字符串str中查找ASIIC码c对应的字符**最后一次**出现的位置
参数:
str:字符串指针
c:要查找的字符(用int强转后传入)
返回值:
如果找到字符c,返回指向该字符的指针
如果未找到字符c,返回NULL
*/

测试代码:**[点击运行]**

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

int main() {
char str[50] = "Halo everyone!";
char *result = strrchr(str, 'e');

//打印返回的指针会显示查找到的第一个字符之后的内容
if(*result != NULL){
printf("strrchr: found at %d\n", result-str);
printf("strrchr result: %s\n", result);
}
else{
printf("strrchr: not found\n");
}
return 0;
}

输出结果:

output
1
2
strrchr: found at 13
strrchr result: e!

字符串匹配函数

strstr

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
char *strstr(const char *str1, const char *str2);
/*
功能:
在字符串str1中查找字符串str2**第一次**出现的位置
参数:
str1:字符串指针
str2:要查找的字符串
返回值:
如果找到字符串str2,返回指向该字符串的指针
如果未找到字符串str2,返回NULL
*/

测试代码:**[点击运行]**

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

int main() {
char str[50] = "Halo everyone!";
char *result = strstr(str, "Halo");

//打印返回的指针会显示查找到的第一个字符之后的内容
if(*result != NULL){
printf("strstr: found at %d\n", result-str);
}
else{
printf("strstr: not found\n");
}
return 0;
}

输出结果:

output
1
strstr: found at 0

字符串转换函数

atoi

将字符串转换为int类型,注意传入的字符串需要是纯数字字符串

其他的如atof、atol、atoll等函数同理,只不过返回值的数据类型不同,如atof是返回float类型,atol是返回long类型,浮点类型在转换的时候会四舍五入。

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
int atoi(const char *str);
/*
功能:
将字符串转换为整数
参数:
str:字符串指针
返回值:
转换后的整数
*/

测试代码:[点击运行]

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

int main() {
char str[50] = "12345";
int result = atoi(str);

printf("atoi: %d\n", result);
return 0;
}

输出结果:

output
1
atoi: 12345

字符串切割函数

strtok

字符串切割,按照delim指向的字符串中的字符,切割str指向的字符串。其实就是在str指向的字符串中发现了delim字符串中的字符,就将其变成\0,调用一次strtok只切割一次,切割一次之后,再去切割的时候strtok的第一个参数传NULL,意思是接着上次切割的位置继续切

注意:

  • strtok函数会改变原字符串,将切割的位置替换为\0,所以如果不想改变原字符串,需要先复制一份原字符串
  • 如果str中出现了连续几个delim的分隔符,则这一次切割会将这一段连续的分隔符替换为\0,所以并不是有多少个分隔符就切割多少次,而是有多少个非分隔符的字符串就切割多少次。
1
2
3
4
5
6
7
8
9
10
11
#include <string.h>
char *strtok(char *str, const char *delim);
/*
功能:
将字符串按照指定的分隔符进行切割
参数:
str:字符串指针
delim:分隔符
返回值:
返回指向切割后的字符串的指针
*/

测试代码:**[点击运行]**

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>
#include <string.h>

int main() {
char str[50] = "1111:2222:3333::444";
char *result;
result = strtok(str, ":");
int i = 1;
//打印切下的第一片的结果
printf("times : %d , result: %s\n", i , result);


while (
(result = strtok(NULL, ":")) != NULL
){
i++;
//打印后续切割的字符片
printf("times : %d , result: %s\n", i , result);
}
return 0;
}

输出结果:

output
1
2
3
4
times : 1 , result: 1111
times : 2 , result: 2222
times : 3 , result: 3333
times : 4 , result: 444

字符串格式化函数

sprintf

将格式化的数据写入字符串中,类似于printf函数,只不过printf函数是将格式化的数据输出到屏幕上,而sprintf函数是将格式化的数据写入字符串中。功能上有点类似于strcpy函数。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
/*
功能:
将格式化的数据写入字符串中
参数:
str:字符串指针
format:格式化字符串
...:可变参数
返回值:
返回写入的字符数
*/

测试代码:[点击运行]

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

int main() {
char str[50];
int a = 10;
float b = 3.14;
sprintf(str, "a = %d, b = %f", a, b);
printf("%s\n", str);
return 0;
}

输出结果:

output
1
a = 10, b = 3.140000

sscanf

竞赛中超级常用的函数,可以从buf中按照指定格式读取数据

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
/*
功能:
从字符串中按照指定格式读取数据
参数:
str:字符串指针
format:格式化字符串
...:可变参数
返回值:
返回读取的参数个数
*/

测试代码:[点击运行]

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
char str[50] = "a = 10, b = 3.14";
int a;
float b;
sscanf(str, "a = %d, b = %f", &a, &b);
printf("a = %d, b = %f\n", a, b);
return 0;
}

输出结果:

output
1
2
a = 10, b = 3.140000


855 词

内存溢出(Out of Memory)

概念

内存溢出指程序在申请内存时操作系统无法再向程序提供足够的内存空间,导致程序无法正常执行。结果是程序线程等待触发操作系统未响应,或者线程直接崩溃。


内存越界/内存踩踏

概念

内存越界是指程序在对指针或者数组等连续内存进行写操作时,没有考虑区域的大小而直接进行写操作,这样就导致其他区域的内存被写入数据,从而导致数据异常甚至程序崩溃

常见内存越界场景

1. 数组越界

1
2
int a[10];
a[10]=1;

即定义了一个长度为10的数组,但是访问了数组第11个元素,导致数组越界,访问了不属于该数组的内存空间,从而发生内存越界。

2. 指针越界

1
2
3
int *p;
p=(int *)malloc(10);
*p=1;

即定义了一个指针,但是没有给它分配内存空间,直接给指针赋值,导致指针越界,访问了不属于该指针的内存空间,从而发生内存越界。
`


内存泄漏(Memory Leak)

概念

申请的内存,首地址找不到了,再也没法使用了,也没法释放了,这种就被称为内存泄漏。

常见内存泄漏场景

1. 指针赋值导致地址丢失

动态内存申请函数返回的指针被重新赋值,或者被其他数据覆盖而导致丢失了原来指向的内存空间地址。

2. 定义函数申请空间后未释放

1
2
3
4
5
6
7
8
9
10
11
12
13
void fun(){
char *p;
p=(char *)malloc(100);
//接下来,可以用p指向的内存了
}

int main(){
//由于fun没有释放内存,导致每次调用都泄漏100字节
fun();
fun();
return 0;
}

即定义了函数中使用了动态内存申请函数,并且将返回的指针存在了栈区,但是在函数结束前并没有释放内存,这样就导致栈区的指针丢失,无法释放了。

3. 经常申请但是很少释放(隐性泄漏)

即申请内存的频率很高,但是很多情况下这些内存无法得到释放,被长时间占用,并且一直申请新的内存空间,这样的程序在长时间运行之后也会内存溢出从而崩溃。

内存泄漏的危害

长时间泄漏内存会导致程序内存溢出

800 词

容易混淆的指针


指针数组和数组指针

  1. int *a[10]

    • 指针数组,本质是数组,数组中的元素是int类型的指针。它加一相当于下标加一。常用于保存字符串。
  2. int (*a)[10]

    • 数组指针,指针指向了一个数组入口,本质是一个指针。它加一的话就是指向数组末尾后的数据。常用于作为二维数组的行指针。

前文中有提到,带括号的都是指针,不带括号的就是各种指针可指向的数据或者函数。

  1. int **p
    • 二级指针,常用于代表指针数组,将指针数组传入函数中

指针函数和函数指针

  1. int *fun(void)

    • 指针函数,返回一个指向int的指针。
  2. int (*fun)(void)

    • 函数指针,指向一个返回int的函数。方便使用函数指针变量将函数作为参数传入。

一些特殊的指针

  1. void *p
    • 表示通用指针,或者未定义类型的指针,任何类型的指针都可以赋值给它。
  • 比如常用的memset函数void *memset(void *s, int c, size_t n);功能就是将s指向的内存的前n个字节全部用c赋值。这里就用了通用指针s,且返回值也是通用指针。
  • 在C99标准中,使用void *p = malloc(sizeof(int))来为这种通用指针赋值从而分配内存被认为是安全的;如果你想要在堆上开辟一片数组,则可以使用void *p = calloc(sizeof(int), 10);
  1. NULL
    • 空指针,未赋值的指针都指向NULL。
  • 比如int *p = NULL;就是将p指向NULL。
  1. void *p = malloc(sizeof(int));

    • 动态分配内存,返回一个指向void的指针,即通用指针。
  2. void *p = calloc(sizeof(int), 10);

    • 动态分配内存,返回一个指向void的指针,即通用指针。

mian函数传参

  1. int main(int argc, char *argv[], char *envp[])
    • argc是命令行参数的个数
    • argv是命令行参数的数组。
    • envp是环境变量的数组;可省略。
1.6k 词

概念

动态内存申请实际上是在堆区开辟内存空间,而堆区空间手动开辟,手动释放,不像栈区由操作系统自动维护。同时,很多时候我们需要的内存空间取决于实际输入的数据,所以我们就需要动态分配内存了。

动态分配函数

  • 注意
    动态内存分配函数的使用需要注意以下几点:
    • 一般用memset初始化
    • 调用函数后一定要判断一下是否申请成功
    • 每次调用函数申请的空间是连续的,但是多次调用函数申请的空间不一定连续。
    • 返回的是void *指针,在调用函数时,需要强制类型转换成我们需要的类型。即-养成调用函数前面接强转的好习惯。
    • 内存空间使用完之后一定要释放
      ,否则会造成内存泄漏

malloc函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
void *malloc(size_t size)
/*
功能:
在堆区开辟指定长度的连续内存空间
参数:
size指定开辟内存空间的大小,单位为字节
size的类型size_t本质是unsigned int的typdef
返回值:
返回指向开辟内存空间首地址的指针(void *),
如果开辟失败,返回NULL
*/
  • 堆区中分配一块长度为size字节的连续存储空间;并返回指向该存储空间首地址的指针,分配失败时,返回NULL。

calloc函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
void *calloc(unsigned int n, size_t size)
/*
功能:
在堆区开辟指定长度的连续内存空间,并初始化为0
参数:
n指定开辟内存空间的个数
size指定开辟内存空间的大小,单位为字节
size的类型size_t本质是unsigned int的typdef
返回值:
返回指向开辟内存空间首地址的指针(void *),
如果开辟失败,返回NULL
*/
  • 在堆区中分配一块长度为n×size字节的连续存储空间;并返回指向该存储空间首地址的指针;分配失败时,返回NULL。
  • malloc只分布内存不负责初始化不同,calloc分配的空间被初始化为0

realloc函数

调用malloccalloc函数单次申请的内存是连续的,两次申请的两块内存不一定连续。为了满足这种需求,即我先用malloc或者calloc申请了一块内存,我还想在原先内存的基础上挨着申请内存。或者我开始时候使用malloccalloc申请了一块内存,我想释放后边的一部分未使用的内存。为了解决这个问题,发明了realloc这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
void *realloc(void *ptr, size_t newsize)
/*
功能:
重新分配内存空间,调整已分配内存空间的大小
参数:
ptr指向要重新分配内存空间的指针
newsize指定重新分配内存空间的大小,单位为字节
newsize的类型size_t本质是unsigned int的typdef
返回值:
返回指向重新分配内存空间首地址的指针(void *),
如果分配失败,返回NULL
*/
  • 如果传入的指针的内存空间
    后面有足够的空间,则直接调整大小,并返回指针;
  • 如果传入的指针的内存空间后面没有足够的空间,则重新开辟一块大小为newsize字节的内存空间,并将原内存空间的内容复制到新内存空间中,并返回新内存空间的指针;
  • 如果传入的指针为NULL,则相当于调用malloc函数;
  • 如果newsize小于传入的指针指向的内存空间,则只调整大小,不改变内容。

free函数

1
2
3
4
5
6
7
8

#include <stdlib.h>
void free(void *ptr)
/*
功能:释放堆区空间
参数:ptr指向要释放的内存空间
返回值:无
*/
  • 释放指针指向的内存空间
  • ptr必须是malloccallocrealloc函数返回的指针,否则结果是未定义的。
109 词

引言


大学过得真快啊,眨眼就到大三了,在搞定了《计算机组成原理》、《操作系统》、《数据结构》之后,我觉得我应该重新学一遍C语言,顺便把C的进阶特性和之前的学C以及做项目时遇到的疑点扫一遍。
以下是我的学习笔记索引。
## 索引

579 词

指针函数


指针函数的定义

  • 本质是一个函数
  • 指针作为函数的返回值

用例

1
2
3
4
5

char * func(){
char *p = "123";//创建变量123
return p;//返回p的地址
}

指针函数的坑

这个指针函数返回的指针指向的数据一定是字符串”123”吗?

如果让刚学完C语言的我回答,我可能会从语气中听出一些端倪,但是要我说个原因倒是不好描述。不过现在回过头来看,底层知识的清明就可以很快找到问题的关键了。

函数对应线程,线程拥有自己的栈空间(操作系统的逻辑内存),函数内的变量自然也是存储在栈空间中的。当函数完成他的使命之后,操作系统就会把栈空间释放掉,而栈空间中的变量自然也就被释放掉了

这会带来什么呢?如果还记得指针的意义的话,就知道指针存的是地址,而不是数据,那么既然栈空间的数据都释放掉了,返回的指针指向的内存自然是没有意义的。或者说会获得一个“脏值”

ok,那么怎么解决这个问题呢?
难道要在外部调用这个函数的线程上就创建这个值吗?这明显不符合函数的要求和目的

其实很简单,static一下就能让操作系统为你的变量保留内存地址了。这也是static的“主要用途”。

1
2
3
4
5

char * func(){
static char *p = "123";//创建变量123
return p;//返回p的地址
}
1k 词

函数指针


函数指针的定义

  • 本质是一个指针
  • 指向函数的指针

引言

OK啊,结合计算机组成、单片机系统基础、操作系统。
不难知道,无论是目前PC的冯诺依曼架构,还是单片机的哈佛架构,程序都是需要空间保存的,只不过两个架构保存的地方不一样而已。

那么,函数当然有自己的指针啦,不然你汇编JMP怎么知道跳到哪一行。

C语言也差不多,C语言中函数的名字就是函数的首地址(入口地址),那么我定义一个指针指向这里当然是一个允许的操作啦~

而这种内容存的是函数入口地址的指针变量,我们就叫它函数指针用来和其他的数据指针区别一下。

顺便说一下,这种“存入口地址”或者“存起始地址”的指针,往往定义的时候喜欢用 类型 + (*指针名)定义,数组指针也这样,函数指针也这样;不加括号就是反过来指针数组,指针函数。(开始晕了)


用例

1
2
3
4
5
6
7
8
9
10

int max(int x, int y){
...
}
int min(int x, int y){
...
}

//定义一个传参为两个int,返回值为int的函数指针。
int (*p)(int, int);

这样就相当于给两个函数都抽象出来了,那么是不是可以做点什么呢……

函数指针的调用

1
2
3
4
5
//接上文代码

p = max;
num = (*p)(3,5);
num = p(3,5);

函数指针的调用可以省略括号


函数指针数组

函数指针常用常用于将函数作为参数传递给另一个函数,这个函数就被称为回调函数

函数指针数组的妙用

当然是用函数指针把功能类似编个组放一起啦!(相当于建立函数库)这样在使用回调函数的时候甚至可以用偏移量来决定调用的函数,就不需要Switch来路由函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

int first(char a){
...
}
int second(char a){
...
}
int third(char a){
...
}

//定义一个函数指针数组
int (*p[])(char) = {first,second,third
}

int n , b;

scanf("%d, %d",&n, &b);

p[n%3](b);


顺便复习一下指针为什么要有类型?

  • 防止对内存的越界操作
    (我改一个2B的变量,结果指针的类型是4B的类型,你猜猜多出来的2B的数据写哪去了?//操作系统:阿米诺斯,编译器你怎么给这操作允许了,给我栈区都干穿了,线程都干开线了。)