当前位置: 首页 > news >正文

C 语言基础知识

字符串

概念

  • 定义:使用空字符结尾的一维字符数组
  • 空字符/结束符/ NULASCII 码表中值为 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;
}


  • 补充:数组名其实就是一个指针,如下打印 site2site4 显示的值是一样的
// 打印指针用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 封装提供的 APIAPI 实现中采用内存池来优化性能,并避免了直接与操作系统交互

  • 申请内存原理:

    • 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);
}

相关文章:

  • windows与ubuntu双硬盘双系统安装及启动(全流程成功)
  • Linux中输入输出管理技巧
  • jmeter线程组高并发(详细讲解)
  • 微信小程序实现动态二维码海报生成与保存 | 高效便捷的前端方案
  • UE5学习笔记 FPS游戏制作33 游戏保存
  • 数据库中的函数:高效操作与灵活运用
  • nut-collapse折叠面板(案例)
  • OSPF协议(数据包刨析)
  • NLP高频面试题(二十七)——SFT有哪几种参数微调方法?有什么优缺点?
  • 开源守护,智护童年——幼儿园未成年行为与安全智能监控系统
  • 贪心算法(14)(java)无重叠区间
  • SSL提供了哪些安全服务
  • 镜头光圈总结
  • Unity加载OSGB倾斜摄影数据
  • Android 确定废弃「屏幕方向锁定」等 API ,如何让 App 适配大屏和 PC/XR 等场景
  • Easysearch 如何短暂维护 Data 节点
  • Ubuntu 22.04安装MongoDB:GLM4模型对话数据收集与微调教程
  • 使用Google Gemini API密钥创建AI驱动的Chrome扩展程序
  • 便携免安装,畅享近 30 种 PDF 文档处理功能
  • 首个物业plus系列展 2025上海国际智慧物业博览会开幕
  • 喀什的网站怎么做/营销推广的形式包括
  • 企业网站建设案例分析/百度竞价推广教程
  • 深圳外贸网站建设/设计一个简单的网页
  • 2015年做哪个网站能致富/索引擎优化 seo
  • 怎么做网站建设作业/百度q3财报减亏170亿
  • 亚马逊做网站发礼物换评价/西安市网站