C 语言基础知识
字符串
概念
- 定义:使用空字符结尾的一维字符数组
- 空字符/结束符/
NUL
:ASCII
码表中值为0
的控制字符,使用\0
表示,用于标记字符串的结束
声明
- 字符数组声明:有效字符数等于字符数组长度减一
int main()
{
char site[10]; // 定义一个能存储10个字符的变量,其中最后一个字符用于默认为空字符,故实际只能存储9个字符,有效字符数为9
}
- 字符指针声明:
char *str;
初始化
- 字符串字面量:被双引号包裹的字符系列,如
"aaa"
长度为3
// 不要将字面量/输入的字符串直接赋值给字符指针;要么给字符指针开辟空间,然后将字面量复制过去;要么直接赋值给字符数组
- 字符数组初始化:
int main()
{
char site[10]; // 定义一个能存储10个字符的变量,其中最后一个字符用于默认为空字符,故实际只能存储9个字符,有效字符数为9
char site[10] = {0}; // equal char site[10] = "0"; 将数组能存储的有效字符都设置为字符0
char site1[10] = "RUNOOB"; // 若指定了数组长度,则赋值时字面量字符数必须小于数组长度,不然 sizeof 求出的长度是一个不确定值
char site3[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'}; // 一定要加空字符,不然 strlen(site3) 有问题,不过数据长度计算没问题
char site2[] = "RUNOOB"; // 会自动计算字面量的字符数,然后将其加一作为字符数组的长度
char site6[] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'}; // 会自动计算字面量的字符数,然后将其加一作为字符数组的长度
// 不能在定义数组的时候使用变量来定义数组长度,因为字符数组创建之后,其长度不能被修改,而变量是可以修改的,所以只能用常量
int NUM1 = 101;
char str1[NUM] = "hello"; // ERROR
const int NUM2 = 101
char str2[NUM2] = "hello"; // RIGHT
}
- 字符指针初始化:
int main()
{
char *str = NULL;
// 只有将字符串字面量赋值给字符指针时,此字符串才会存储在静态存储区,存储在静态存储区的字符串是不被允许修改的
char *strPointer = "test"; // 不要这么做
// 开辟空间并复制
char *site2 = malloc(strlen("RUNOOB") + 1);
strcpy_s(site2, strlen("RUNOOB") + 1, "RUNOOB");
}
修改
- 字符数组:已经赋值过的字符数组不能像
Java
一样直接使用赋值运算符将新字符串赋值给字符数组,得使用复制函数;不过可以直接修改字符数组中元素
char oldValue[20] = "hello";
char newValue[20] = "hello world";
strcpy(oldValue, newValue); // right; note: newValue lenght < oldValue lenght
printf("%s\n", oldValue);
oldValue = "test"; // error
- 字符指针:可以直接赋值,也可以直接修改所指向的字符串中的字符,但前提不是使用字符串字面量直接赋值给字符指针
char str1[20] = "hello world";
char* str = str1;
str = "hello!!!";
printf("str value: %s\r\n",str);
计算
strlen
- 作用:计算字符串
str
有效字符数;本质上就是遍历字符串直到遇见空字符结束---
空字符不参与计数;若没有空结束字符,就会出现问题 - 原型:
strlen( const char *str )
char str[] = "abc";
int len = strlen(str);
printf("%d\n",len); // 打印3
char str[5] = "abcde";
int len = strlen(str);
printf("%d\n",len); // 打印5
sizeof
sizeof
:返回一个变量/类型所占的内存字节数
int a = sizeof("1") // a = 2
char str[] = "A";
int num = sizeof(str);
printf("%d",num); // num == 2, 因为创建字符数组时,未指定长度,故计算时会把结束符'\0'算进去
char str[] = {'a','b'};
int num = sizeof(str);
printf("%d",num); // num == 2, 因为使用花括号创建字符数组时,未指定长度,故计算时计算花括号中的字符数,若花括号中有空字符,则也会计算进去
char str[5] = "abc";
int len = sizeof(str);
printf("%d",len); // len == 5, 因为创建字符数组时指定了长度为5
打印
- 打印:打印字符串不会显示空字符
\0
char str1[] = "hello world";
printf("%s",str1); // 打印hello world,而不是hello world\0
char *str2 = "hello world";
printf("%s\n",str2); // 打印hello world,而不是hello world\0
做参
- 字符串打印:不要将字符串字面量/输入字符串直接赋值给字符指针,然后传参
void test(char *str)
{
printf("%s\n",str); // 正确打印
}
int main()
{
char str2[] = "hello world";
test(str2);
}
- 字符串修改:不要将字符串字面量/输入字符串直接赋值给字符指针,然后传参
void updateChar(char *str)
{
str[0] = 'a';
printf("%s\n",str); // 正确打印
}
int main()
{
char str2[] = "hello world"; // 不能用字符指针接受字符串字面量,必须使用字符数组
updateChar(str2);
printf("%s\n",str2); // hello world
}
- 修改变量指向的字符串:若要将传入的指针指向指向新字符串,必须使用
char *
类型指针的地址作为函数实参,使用二级指针作为函数形参;因为字符数组一经初始化就不能修改
void updateAllError(char *str)
{
str = "hello"; // 让str指向了新字符串所在的地址,即修改了str的值
printf("%s\n",str); // 正确打印
}
void updateAllRight(char **str)
{
*str = "hello\0"; // 修改了传入的存储字符指针变量的地址中的值,即修改了入参的值,让其指向新变量
printf("%s\n",*str); // 正确打印
}
int main()
{
char str1[20] ;
scanf_s("%s",str,sizeof(str));
char *str2 = str1; // 必须,不然会出错
updateAllError(str2); // hello
printf("%s\n",str2); // hello world
updateAllRight(&str2); // hello
printf("%s\n",str2); // hello
}
数组
- 初始化:若使用花括号初始化,花括号中的值会对应赋值给数组,数组剩下的元素默认用
0
初始化;字符数组/指针数组也是类似的
int main(void) {
int arr[5]; // random value
int arr1[5] = {0}; // 0 0 0 0 0
int arr2[5] = {1}; // 1 0 0 0 0
int arr3[5] = {1,1}; // 1 1 0 0 0
for(int i = 0; i < 5 ;i ++) {
printf("arr[%d]: %d\n",i,arr[i]);
}
return 0;
}
- 补充:数组名其实就是一个指针,如下打印
site2
和site4
显示的值是一样的
// 打印指针用p
printf("%p\n",site4);// 0000000898dff752
printf("%p\n",site2);// 0000000898dff752
指针
- 指针初始化:不知道用啥初始化就用
NULL
- 数组与指针:数组变量本质上就是指针常量,数组变量一经赋值,其值就是数组的首地址,其值不允许被修改
char str[10] = "hello"`
str = "world"; // error, 数组变量str的值不允许被修改
- 常量指针:指针变量向地址中的值不允许修改,
const
写在类型前:const int *p
- 指针常量:指针变量的值不允许修改,
const
写在类型后:int const *P
- 指针数组:变量名表示一个数组,该数组中的每一个元素都是指针:
int *p1[3]
,p1
是一个指针数组,数组元素为int *
类型/int
型指针 - 数组指针:变量名表示一个指针,该指针指向一个数组:
int (*p)[10]
,p
是一个数组指针,指向一个int
型数组,该数组有10个元素 - 结构体指针:变量名表示一个指针,该指针指向一个结构体
typedef struct{
int age;
char name[16];
}Person;
int main(){
Person liu = {22,"liu"};
Person *li = malloc(sizeof(Person));
// li = &liu;
}
-
函数指针:变量名表示一个指针,该指针指向一个函数
- 定义格式:
int (*p)(int x, int y)
- 语法说明:定义一个名为
p
的函数指针,该指针可以指向具有该种模式的函数,模式:有两个参数,参数类型均为int
,返回值类型为int
- 使用案例:
- 定义格式:
// 定义函数
int maxValue (int a, int b) {
return a > b ? a : b;
}
// 定义函数指针,指向的函数模式与maxValue函数模式一致
int (*p)(int, int) = NULL;
// 指向函数
p = maxValue;
// 指针调用
p(20, 45);
内存知识
内存空间布局
- 程序与进程:C代码经过编译后就生成了存储在磁盘上的程序,运行程序就产生了进程
- 程序组成:通常都是由
BSS
段、data
段、text
段三个组成的
注意:如果用
0
初始化全局/静态变量,等于没有初始化,因为默认为0
函数栈与变量
- 栈与函数:
C
程序始于main
函数,当执行C
程序时,会先执行main
函数,此时操作系统会为C
程序分配一个函数调用栈,同时将main
函数的函数调用类型对象入栈;若main
函数中调用了函数A
,则会将A
函数的函数调用类型对象压入栈中;若函数A
执行完毕,则会将A
函数的函数调用类型对象出栈;如此,执行一个C
程序,会得到一个函数调用栈,栈中每个元素都表示一个函数调用类型对象,该对象存储了该函数中定义的变量,以及函数签名中的参数
参考文章:C语言内存模型-CSDN博客
- 栈与变量:函数中定义的变量存储在函数调用类型对象中;当函数被调用时,会为其创建一个函数调用类型对象,并将其压入函数调用栈;当函数执行完毕,会将该对象出栈,该对象的入栈时间和出栈时间差就是函数中定义的变量的生命周期
重点:对于函数中创建的指针/数组变量
A
,在函数调用结束后,其生命周期就结束了,存储变量的地址被回收,其指向的内存地址也会被操作系统回收,变成系统内存,这段内存不允许再被程序访问,除非重新分配给程序;此时若再定义指针/数组B
,则有一定概率将之前A
所指向的内存地址分配给B
;错误案例如下所示
void func1(char *str, char *data[])
{
char comBufTmp[256] = {0};
char* test = str;
memcpy(comBufTmp, str, 256);
char delim[] = " ";
int index = 0;
data[index] = strtok(test, delim);
while (data[index] != NULL) {
index++;
data[index] = strtok(NULL, delim);
}
}
void func2()
{
char stack[2048];
memset(stack, 0, 2048);
}
int main()
{
char str[] = "test test test test test test test test test test";
char *data[10];
int data1[10];
int data2[10];
func1(str,data);
for (int i =0; i < 10; i++) {
data1[i] = *(int *)(data[i]);
}
func2(); // 会清掉comuffTmp所指向内存的值
for (int i =0; i < 10; i++) {
data2[i] = *(int *)(data[i]);
}
for (int i =0; i < 10; i++) {
printf("data1[%d]=%d, data2[%d]=%d\n", i, data1[i], i, data2[i]); // data1 和 data2 不一样
}
}
char* func1()
{
char str[256] = {0};
return str;
}
int main()
{
char* str = func1(str); // ERROR
}
Glibc
内存管理
-
Glibc
内存管理:Linux
下的内存管理程序,C
语言的malloc()/free()
是Glibc
封装提供的API
,API
实现中采用内存池来优化性能,并避免了直接与操作系统交互 -
申请内存原理:
malloc()
申请内存时,Glibc
会判断内存池中有空闲内存:- 若有:则返回内存池中的内存块
- 若无:通过
brk/mmap
系统调用去操作系统申请,释放时也先释放到内存池中以备下次使用
-
Glibc
返回给用户的内存块内部用Chunk
对象管理,它给用户使用的有效内存块前后加了一些控制信息,来记录分配信息,例如内存大小、与其它内存块的连接,以方便完成分配和释放工作;Chunk
对象用分箱式内存管理方式,分配时从空闲并合适大小的链表中获取
常见内存操作
内存申请
- 申请内存大小问题
malloc
参数是无符号类型,传入参数<0
会转换成大整数malloc
参数为0
时,可能返回非空值,影响后续程序判断malloc
时如果使用外部参数,一定要判断参数有效性
-
没有判断
malloc
返回值malloc
可能失败,失败时返回NULL
,不判断返回值可能导致引用NULL
指针问题malloc
一定要判断返回值是否为NULL
,并对返回NULL
的场景设计完善的错误处理
-
在中断上下文中使用
malloc
malloc
过程可能sleep
,等待别的线程完成内存回收- 中断上下文不能被抢占,回收线程可能得不到执行,导致死锁
- 避免在中断中使用
malloc
内存引用
-
指针引用问题
- 引用
NULL
指针,程序崩溃 - 引用未初始化指针,可以指向任意地址,可能崩溃或数据错误
- 引用已释放指针,该地址可能已经被其他变量使用,导致数据覆盖
- 指针初始化、释放后都置为
NULL
,引用时检查指针的有效性
- 引用
-
指针偏移问题
- 指针偏移超出了分配空间,可能引用其他变量地址,导致数据错误
- 指针类型与分配时不一致,偏移量计算会出错,导致数据错误
- 注意数组下标(特别是字符串结束符),谨慎使用指针类型强转
- 使用安全函数,并正确填写
buffer_size
参数 - 仅进行地址偏移计算,不进行内存引用,不会发生问题,如计算
struct
成员偏移量
内存释放
-
动态申请的内存未释放
- 申请的内存不释放会导致内存泄漏,相应内存空间一直被占用
- 内存申请指针必须释放、返回或保存,否则一定有内存泄漏
- 通过函数返回状态要能判断内存状态,避免使用
realloc
-
free
非动态申请的指针free
只能处理堆空间,全局变量地址和栈地址不能使用free
释放- 不能对偏移过的指针进行
free
,可能导致内存泄漏 - 区分栈指针和堆指针,尽量不要对内存申请指针进行偏移操作
-
重复释放同一地址
- 释放后的地址可能已经被其他变量使用,再次释放可能会导致其他变量的数据被覆盖
free
后将指针变量置为NULL
,避免重复释放
并发访问
-
申请后使用,使用完释放,不能重复释放
-
共享地址大小、类型要统一
- 向其他线程共享栈空间地址
- 函数或线程退出时栈变量销毁,其他线程不能再访问共享变量
- 函数或线程退出前需要等待其他线程使用完毕
- 通过引用计数确认其他线程是否还在使用
-
与其他线程共享堆空间地址
free
释放后所有线程不能再访问共享变量- 最后一个使用者通过
free
释放共享地址 - 通过引用计数确认最后一个使用者
循环读
数字循环读
int a;
while(scanf("%d",&a) != EOF){
printf("%d\n",a);
}
字符循环读
char a;
while(scanf("%c",&a) != EOF){
printf("%c\n",a);
}
while((a = getchar()) != EOF){
printf("%c\n",a);
}
字符串循环读
const int NUM = 101;
char str[NUM];
while (scanf_s("%s", str, sizeof(str)) != EOF) {
printf("%s\n",str);
}
while ((int)gets(str) != EOF) {
puts(str);
}