C语言万字精华总结

声明和定义区别

声明是让编译器和其他源文件认识这个符号。 放在头文件里管理以便在多个源文件中共享

定义是告诉编译器给符号预留内存大小。 告诉编译器符号的类型和名字,编译器会记录符号的内存大小和地址。 放源文件里管理。

因此一个符号可以在多个地方声明,但只能在一个地方定义。


变量和常量

常量:以双引号开头\0结尾的字符串常量(字符指针)和字符常量和转义,各个进制数和普通数字,浮点型的科学计数法,布尔常量,枚举常量,大部分是=号右边的
变量:字符串变量(字符数组),=号左边的字母,const修饰的对象
1.局部变量和常量和全局 静态 动态的分配内存的阶段

总结:

常量和宏定义都在预编译阶段分配内存

全局变量和静态变量都是编译阶段分配内存和初始化为0(如果没有初始化的话)

所有变量都是编译阶段分配内存(除了函数内部变量和全局静态变量除外)

函数内部变量在运行时进入函数才开始分配空间

局部变量:被编译器预留内存(开始分配内存),并放入目标文件的段中,运行阶段被系统映射到栈区。当函数被调用时,栈帧会被创建,因为编译器不会进入函数内部找符号,局部变量的内存会在栈上分配,并在函数返回时释放。

静态和全局变量被编译器预留内存(开始分配内存),并放入不同的(已初始化、未初始化)的段,运行阶段被系统映射到静态区即分配内存完成

动态空间变量:在运行阶段才开始分配内存

2.左值与右值

左值指可以取地址即已存在地址的变量,右值指无法取地址的没有实际地址的值

3.常量 静态 全局 成员的命名规则

常量全大写

const int MAX=100

静态变量前面加 s_(表示static)

全局变量前面加 g_(表示global)

类的成员前面加 m_(表示member成员)

长的单词小驼峰命名法

4.变量的规则:一次只能修改同一个变量的值,不同变量不影响

一次只能修改同一个变量的值,不同变量不影响 i=i++错误;赋了值i的值又变了,应该改为i+=1或者i++ c=a[i++]就合法是因为i和c不是同一个东西,不影响就合法

5.常量编程规范

尽量不用宏定义的常量,宏可读性差,维护难

把不同文件的常量集中在一个公共的头文件中

如果某个常量与一个东西相关,不应该给一个别人看不懂的值

const float MAX=100 错
const float MAX= sum+min //包含关系 对


数据类型

1.整型和整型格式说明符

%hd %ld (2字节s,4字节int,4字节long,8字节long(Windows为4),8字节long long float4

整型格式符(%p十六进制地址,%x八进制 ,%u无符号整型, %d有符号整型)

2.字符型

(转义(加\反转义,字符(int大小),字符串字面量(一个元素一个char大小,总和)

3.实型

(小数点

4.类型的规则:
int a=1000,b=1000; long int c=a*b;  //想用大类型来接收大数值错,类型不一致

除去变量剩下的都是类型包括数组,无论做什么类型都必须一致,除非强转,普通变量不要强转成指针,反之如此,可以考虑用联合体或者其他方法

5.精度比较保护规则

与无符号运算都转成无符号,低字节转高字节运算(char转int,int转浮点等),目的是减少运算误差

6.各类型与0值比较

bool布尔类型与0值比较:false是0 和 true是1他们是宏定义,在 <stdbool.h> 头文件中。引入这个头文件后,可以使用 truefalse 来表示布尔值。

写法应该:
bool b=false或true;
if(b)或if(!b)
    不用b==0或b==true的原因是与0比较被误认为是整型,而false和true是头文件里的宏定义会根据不同编译器造     成不同的值,如有的true是0值有的是1值,不能统一所以不用

浮点型和0值比较:高精度向低精度转从而丢失精度

#include <math.h>
// 定义 一个精度
#define EPSILON 0.00001//这个精度是我所能接受与0值比较的误差范围
​
float f = ...;  // 你的浮点数
​
// 如果 f 的绝对值小于 EPSILON,则认为 f 等于 0
if (fabs(f) < EPSILON) {//fabs()绝对值函数
    // f 等于 0
} else {
    // f 不等于 0
}
    而不用if(f==0.0),比如很大的数+很小的数=很大的数+0;因为高精度向低精度转丢失精度。原因是在二进制表示下,许多十进制的有限小数(例如0.1, 0.2等)变成了无限循环的二进制小数,无法以绝对精度存储。这就可能会导致精度丢失。

指针变量和0值比较:NULL等价于(void)0空地址 NULL 是一个表示空指针的宏,而 0 是整型常数。NULL 的类型为 void ,而 0 的类型为 int。在指针上下文中,NULL 和 0 都可以用作空指针常数,但常规建议是使用 NULL 替代0,使代码更具有可读性。在条件判断中,NULL 和 0 都会被视为 False。

if(p=NULL)   不要if(p)或p=0这种被人误认为是布尔类型或整型

符号

1.优先级

【】 () . -> -减号 (强转类型)

++ -- * & ! ~ sizeof 算术 位运算符 关系逻辑条件赋值

规则:每个表达式都加括号防止优先级问题

陷阱:/和*之间没有括号都会被当成注释

y=x/*p     改成x/(*p)才对
2.自增自减本质是自带赋值和移动
j=(i++,i++,i++);   //意思是每一个逗号之后都会i++,但j只取最后一个i++的结果,实际结果就是比最终的i少1
3.位操作符和运算符sizeof

位操作符(>>  <<  &  |  ^  ~)

sizeof(为什么int [2]占8大小?因为sizeof用于计算变量或类型大小)

4.三目运算符

返回一个只读值

int a = 2; int b = 3; int c = 0; (a < b ? a : b) = c;//左边是一个值,值是只读的不可被赋值 改成*(a < b ? &a : &b) = c;
5.注释

的就近规则和反转义加\(全局 宏定义一定加注释,注释应讲为何做,作用域加注释告知,函数入口给注释和断言

注释不能嵌套,因为和就近的匹配,和else与就近的if匹配一样**
​
 ""双引号也是遵守就近原则,如"left  "ab"  right"    本来想把ab字符串包裹在一个大的字符串,结果就近原则导致ab不在字符串内了

/*ab/*非法的/*/*                将会输出:非法的
 
转义:纠正上面的错误问题要转义,把其变成普通字符不在具有原来的符号功能,加\
6.接续符\的作用和规则

(代码太长,接续下一行。 接续符本行后面不能加任何符号包括空格

c:\english\           本行后面不能再接任意字符,需要换行
_to_this_3
加上空格就会改变这个含义。反斜杠后面的空格(或其他字符)会使编译器(或解释器)认为这一行代码在反斜杠后面结束了,下一行是一个新的语句。
​
这是因为,编译器在处理代码时,通常会忽略空白字符(如空格、制表符和换行符)。然而,当反斜杠出现在行尾时,它告诉编译器"不要忽略接下来的那个换行符,就当它是一个普通字符,而且这一行还没结束"。但是如果反斜杠后面有空格,那么反斜杠就不再是行尾字符,编译器就会认为这一行在空格后结束,所以后面的代码被当成新的一行。

语句太长时符号放在新行开头突出

逗号运算符的陷阱(数组初始化元素的时候


32个关键字

32关键字规则:

关键字后面空一格,以突出关键字

(volatile关键字可以使对象可被硬件或系统改变,且遇到release版本不会优化,const是软件上的不可改。typedef的2种使用方法1对结构体可以加* ,2对普通类型不能加*但意义一样,即结构体可以普通结构体改名结构体指针名字,普通指针类型改名普通名字

const:表面上的就近原则,不会深究。 本质还是变量,c++为常量
typedef char *c; const c p;  //等于const p

想变成真正的常量,用#define或enum枚举

typedef:不要把typedef当转换类型

不能取关键字的类型名:typedef int* int错

取的别名后面不能有任何符号  typedef int* node*和 typedef struct s * 都是错

typedef会遵守作用域规则

typedef struct{
​
}*s;                         
typedef char *c;   
 这两种都是*号在前面,这个星号是跟类型一起的不属于变量,所以是把指针类型取别名而已
    星号在变量后面就错
typedef会做语法解析:
typedef int* IntPtr;
IntPtr a, b;
两个都是指针类型,普通方法想要达到此相同效果正确定义必须变量自己带*号才行
比如int* a,b;  这样定义是不符合预期的,实际上只有a是指针类型,而b是int类型

static和全局

全局是整个程序包括其他源文件都能访问

静态只能在本程序访问

C语言的static:

不能修饰结构体成员,只能修饰变量和函数

修饰函数静态就是变成内部链接。作用和全局相反,全局就是外部链接能被外部源文件访问,

修饰变量:作用域不变且延长生命周期

保持该生命周期至return结束,作用域在大括号内结束


循环和分支

while相当于循环多次if

switch最好有default可读性,不需要时可以default:break;

else必须紧跟if

if ((t & 1) == 1)
{
    count++;
}
t >>= 1;//报错
//else不匹配if.下面这个代码没必要存在
else{
    continue;
}

goto(标签名: goto 标签名;跳转,标签名后面没有分号)


函数:

函数的形参是普通变量和返回值是普通变量时都是副本
1.函数的作用

封装,模块化、易维护,代码重用简化调试和测试避免命名冲突

函数不加返回类型默认为整型而不是void

2.值传和址传的区别

值传是临时拷贝的备份,不是实参本身,不会改变实参的内容.

指针值传:形参指针指向实参的值即变量的地址,相当于形参指向变量的指针,可以修改指向的变量的内容

址传:临时拷贝一个形参指针,指向实参的地址,所以能够改变实参的内容

指针址传:分清指针本身和指针指向的内容,需要二级指针形参指向一级指针实参的地址,才能改变实参指针的指向

3.返回值返回到调用函数的身上,返回类型看成返回值的类型就行

返回类型其实就是返回值接受的类型

int* f(void)
{
    int a=0;
    return &a;
}
int *p=f();    遵守类型一致原则

4.调用函数在内存中调用的过程

参数传递:首先需要将参数传递给函数。这通常通过将参数值复制到调用栈(Call Stack)上实现。

栈帧分配:每当一个函数被调用时,一个新的栈帧(Stack Frame)会被创建并压入调用栈。这个栈帧包含了函数的局部变量、参数以及其他一些必要的信息(如返回地址)。

执行函数体:一旦栈帧被设置好,程序计数器(Program Counter)会跳转到函数的起始地址,开始执行函数体中的代码。

返回值处理:函数执行完毕后,如果有返回值,这个值通常会被放置在一个特定的位置,比如寄存器或栈帧的某个部分,以便调用者可以访问它。

栈帧释放:函数执行完毕后,当前函数的栈帧会从调用栈中弹出,释放分配给该函数的内存资源。程序计数器会跳回到函数被调用处的下一条指令,继续执行剩余的程序。

函数名:函数的入口地址

因此函数名和取函数名地址是一样的

6.回调函数,简简单单讲清楚

总结:此函数做实参,在内部调用该函数

函数做实参传给主函数,函数指针形参接收,指向实参函数,在内部判断条件调用该函数,达到设置的条件再调用,灵活

7.递归怎么用和含义

类似嵌套循环(最好去学数据结构与算法的递归思想而不是简单看看)

用法通过形参的改变,来改变内部执行的内容,与实参无关实参是固定的。

函数规则:

1.每个函数都必须有注释,入口处一定要有断言

#include<assert.h>
void f(char* p){
    assert(NULL!=p);
}

2.参数的类型虽然可省但不要省。因为有默认参数提升规则,可能不可测

3.递归要展开,展开到最后一次,然后先执行最后一次逆序执行即先内层语句再到外层

4.尽量将值传递改成const址传递!!节省内存!

5.不建议用static修饰函数,限制了代码的复用性,难以管理和扩展

返回栈地址错误:

函数返回在栈上的局部变量的地址。 返回普通变量没事因为是副本

int* max(void)
{
    int a=0;
    return &a;//栈错误
}
int *p=max;//那块空间已经被销毁了

函数扩展:

怎么让函数返回多个值:

法一:址传递

//通过形参改变实参实现返回多个值的效果
void calculate(int a, int b, int *sum, int *乘法) {
    *sum = a + b;
    *乘法 = a * b;
}
int main() {
    int a = 5, b = 10;
    int sum, 乘法;
    calculate(a, b, &sum, &product);
    return 0;
​

法二: 用结构体接收在函数里修改的多个值,因为结构体是复合类型能存多个数据

在函数里修改结构成员的值即可
返回类型type function(int a, int b,结构体 ) {
   对象.sum = a + b;
   对象.乘法 = a * b;
}
 function(5,10,结构体);

法3:嵌套调用函数     发4:把函数里修改的值传给全局变量(没人会这么做,巨大的副本开销)

回调函数就是一个函数在适当的时刻调用其他函数

用法:

副函数指向其他函数副函数做实参传给主函数,主函数用函数指针形参接收,指向实参函数,在内部调用副函数。相当于a函数利用b函数调用c函数

函数指针当实参时不用加括号,因为函数名是地址。

打印时用回调主函数返回值

 double sum1 = huidiaohanshu(ptr, p, n);//打印的都是用回调主函数返回
ptr和p都是函数,n是int,然后打印sum1就行了

回调函数就是用一个函数来调用其他函数而已

double odd(int n) {
    int i = 0;
    double sum = 0.0;
    for (i = 1; i <= n; i++)
    {
        if (i % 2 == 1)
            sum += (1.0 / i);
    }
    return sum;
}
double dual(int n, double (*func)(int)) {
    return func(n);
}
 double (*func_ptr)(int) = odd;

用函数调用函数和直接用函数指针调用函数的区别

1用途。函数指针通常被用来实现多态或者代码的模块化。而回调函数则通常被用来实现更为复杂的功能,如异步操作、事件处理、自定义排序等。

2控制流。当我们直接通过函数指针调用函数时,我们可以直接控制何时以及如何执行这个函数。而当我们使用回调函数时,什么时候以及怎样执行这个函数(最终的执行过程)通常是由声明这个回调的代码所控制的。也就是说,我们是把执行的控制权交给了另一个函数。所以可以用回调函数内部判断条件和循环来执行其他函数

函数其他:

函数调用参数的求值顺序不确定:

printf("%d %d",f1(),f2()); 有可能是f2()先,f1()后

main返回 0 通常表示程序成功执行,而非零值通常表示程序遇到了错误。

不能嵌套定义函数,因为编译器不能进入函数内部找符号

小心getchar的返回值类型是int

return的含义:终止


数组:

概念:同一数据类型的集合,看做一种数据类型

列不能省原因:指针或数组移动不知道要移多少位
1.数组名什么时候是首地址const指针常变量,什么时候是数组整个地址

只有sizeof(单独的数组名)和&数组名才是代表整个数组,其他都是指针常变量*const

int a[3]={1,2,3};
sizeof(a+1)//不是单独数组名,所以a是首地址即指针,a+1是指针移动一个单位在计算其指针大小,32机器就是4

数组名++是常量赋值会报错,数组名+1没有真正移动不报错

 int a[] = {1,2};
 int* p = a;
 printf("%d\n", *(a+1));//+1只是单纯的操作下一个地址,没有改变a的值
 printf("%d\n", *(++a));//++改变了a的值报错不可修改的左值

2.字符数组的概念

是字符串的本质,是由一个个字符在连续空间上组合而成,需要留一个元素大小存放\0

3.数组什么时候会退化成指针

数组被要求做地址时退化成指向首元素指针(如做函数参数,因为拷贝需要占用大量空间)

4.数组的增删查改

数据结构与算法的知识

5.如何创建动态数组和柔性数组和变长数组
给二级指针(看做二维数组,一个数组分成多份一维数组)指向一个动态开辟的数组,这个数组可以分成多个
一维数组的数组的长度
 
 int **arr = (int**)malloc(行数 * sizeof(int*));
   if (NULL==arr) {//防写成=导致错误
       printf("分配失败\n");
       return 1;//返回0代表程序正常结束,否则异常终止
   }
   // 为每个一级指针(看做一维数组)用循环分配内存
   for (int i = 0; i < 行数; i++) {
       arr[i] = (int*)malloc(列数 * sizeof(int));
       if (NULL==arr[i]) {
           printf("分配失败\n");
           return 1;//返回0代表程序正常结束,否则异常终止
       }
   }
     // 释放malloc次内存
   free(arr[0]);
   free(arr);
//扩展,n维数组就用n级指针来指向动态分配内存的空间,就用n-1次循环嵌套分配内存,就要释放n次内存
​

数组扩展:

数组对应的指针是什么样的

数组的下标是指针的步长

f(int a[3][4])    等价于f(int (*p)[4])
f(int a[3[4[5]]])   等价于f(int (*p)[4[5]])

1.哨兵机制:在数组最后放入一个特殊值,就不用知道数组长度也能进行操作了,
int a[1,2,3,4,-1];
 if(a[i] != -1) {
 }

2.不给声明超大数组是电脑配置低,不给用这么多内存

double a[256][256] //有些电脑不支持,可以用结构体指针数组代替

二维数组

概念:不存在多维数组,只是一整个空间分成多个一维数组的数组的长度,即数组的数组

初始化(列不可省,因为列是一个一维数组。几行就几个一维数组。大括号方式:一维一个大括号包裹)

数组名:a表示二维数组的首行地址&a[0], a[0]表示首行首列地址&a[0]0]

传参格式(max(int arr[]列不可省略] ,int 行,int 列)) 列不能省原因:指针或数组移动不知道要移多少位

重点:二维字符数组

和指针数组的使用场景类似,用来操作多个字符串,但区别是1.灵活性指针所指向的数据可以分布在内存的任何位置。2.类型

题目:把3个字符串升序排序

二维数组扩展:

二维数组被需要是地址时(如传参)被退化数组指针(一维数组指针 二维数组传参时降维成一个数组指针,它所指向的是一个大小为5元素类型为int型的数组,这里我个人认为其实不需要知道它是一个什么类型的指针,,最简单的办法是将第一维写成指针的形式,后续就是其所指内部元素的类型了,int (※arr)[5],,如果是三维数组的话假设它写成int arr[1]2[5],,根据这种方法,我们将第一维直接写成指针的形式,,后续就是其所指内部元素的类型,int (*arr)2这样就是三维数组传参时降维成指针的形式了。这种方式应该是最简单最有效的方法了直接无脑写


字符串

C语言字符串是指双引号扩起以\0结尾的字符数组

1.字符串的概念和字符串字面量的概念

一个是字符数组可以改字符串内容,一个是字符指针不可改内容

2.字符串函数和字符函数和内存函数

内存函数(memcpy set cmp move

字符串函数(tok error str

字符函数(tolower大写转小 toupper小转大

getchar返回值可能是EOF(-1),所以要用int来接收,char不在-1范围会出问题!

int ch=0;
while(ch=getchar(ch)!=EOF){} 或者while(~ch=getchar(ch)){}

3.字符串的增删查改

修改:1.把字符串复制到动态开辟的内存上 2.用字符数组

字符串扩展:

复制(strcpy等)后的字符串常量副本还是常量

strlen是计算数组有效的实际长度,a【100】=0都不算长度


指针:

1.高级指针的应用场景

指针数组

一般不用于存放任意类型的数据的地址,因为一般用结构体,另有作用

数组指针

函数指针

函数指针数组

二级指针

2.指针指向字符串的含义

是字符串字面量

字符数组是有内存空间分配的连续的,所以是一个个字符组成的数组自然是变量

而字符指针只是指向字符串,没有分配空间,所以这个字符串是已经在编译器分配好的常量

重点:3.指针步长及指针数组的移动

指针移动的步长取决于类型的大小。 指针类型的大小决定指针步长和指针解引用访问空间的大小

int num = 10;
char *ptr = &num;
*ptr只能访问num1个字节的内容,取决于大小端

指针数组移动:因为数组也是类型,所以按类型来走步长

int a[3][4不可省略] = {{0}};
int (*p)[4不可省略] = &a;
p+1等价于a[0]+1*[4]等于a[1]跳了4个元素,即到了第二行

结构体指针+1跳过整个结构体

4.解引用含义

解引用后控制权就不再是指针,而是指向的变量

-> 等价于 (*).

5.传值和传址的区别

传值:临时拷贝

传址:间接达到本体效果,引用才是本体

6.void指针的含义和规则(void能接受任何,但别人接受不了void

void*不是通用指针类型,是保存其类型的指针

void指针不能++——、解引用,除非强转

void无类型,不知道变量的内存放在哪里,所以不能创建变量实例,void代表一种抽象的类型,理解了面向对象的抽象基类的概念就容易理解void了

void a是错的

7. 0指针的作用和规则

作用:初始化为0,不指向任何,可防止悬挂或野指针

规则:尺子上刻度为0的地址,因此不能被使用

NULL只能用于指针,相当于 (void*)0

0地址是空指针

8.野指针和悬挂指针

野指针:指向和使用未知的未定义的。发生在(未初始化,指针越界访问)

悬挂指针:指向和使用已经销毁的。相当于宾馆房卡过期了还赖在宾馆住,发生在(指针被free后没赋予空指针,函数返回栈地址

重点:9.理解指针的内存空间和指针指向的内存空间的区别

指针的内存:指针变量的内存空间

10.指针的概念

:是地址,地址的概念:内存单元的编号

现代操作系统使用虚拟内存管理技术,通过目标文件的段将虚拟地址映射到物理地址。这样做的好处包括提高安全性(不允许一些只读代码被修改)、允许内存的非连续使用,以及使多个程序能够同时运行而互不干扰。

变量都会开辟空间分配一个地址,那个地址是不能被改变的,他是变量的房子 其实每个人的地址都不能改变

指针扩展:

1.结构体指针

指向结构体首成员的第一个字节的地址

所以结构体指针+1访问跳过自身一整个结构体

2.给地址强转成(普通基本类型)会怎么样
int a【】={1};
int* p=(int)a+1        a转整型会变成地址常量(由进制构成的),然后地址在+1
整型包括常量和地址常量。     常量的地址都是根据数值相对的(int a=1,地址为0x01),如果一样的数值那么由定义的顺序不同影响内存布局的顺序不同而地址也会变(int a=1;int b=1;a地址为0x01,b地址为0x02),而变量的地址是系统自动分配的不知道的

面试题可能考,不过不要这么写,要么用联合体或想其他方法

3.指针指向自己设计的地址

只有指针才能随意指向自己设计的地址,前提是这个地址是可用的合法的,可以提前用一个变量去试探这个地 址可不可用,在把那个变量删去,用那个变量的地址

int i=10;//调试地址时假设发现是0x11,证明这块地址可用,然后把这个变量删去
int* p=(int*)0x11         0x11是地址常量,进制都属于整型    就可以指定p间接指向自己设计的地址了

但是普通变量和常量不可以自己分配地址,因为这是直接的不行,编译器已经帮定好内存了

4.
如float *f=3.14是错误的,3.14基本类型不会分配内存
定义指针时编译器只为指针自己分配内存,不会给指向的对象分配内存,一个例外是:**指向字符串常量时**,因为字符串常量是不可修改的,如果不为其分配内存,则可能会导致程序崩溃或不可预测的结果。字符串常量在编译时就会被分配到内存中,因此可以保证其在程序运行期间始终存在。

数组的下标是指针的偏移量

不要把普通类型转成指针类型,把指针类型转成普通类型,可以使用联合使这两个共存

int i;
(int*)i  不好
建议union
{
    int i;
    int *a;
}


内存分配

内存池的概念:操作系统把一些内存放入内存池供动态分配使用,没有了系统再放进去。malloc的空间都在内存池里,释放了也会返回到内存池供后续使用,减少了系统调用的次数,直到程序结束才回收。所以free后内存占用没有明显减少是因为这个,作用:提高性能、效率

1.分配的成功或失败要判断==NULL
2.malloc和free成对

申请和释放次数匹配,将释放后赋为NULL

3.内存分配的常见错误

3个错误(同一个动态空间多次free。泄露。只释放一部分。返回栈地址。) free释放的是指针指向的空间,不是释放指针变量的地址,所以最后赋空指针

4分配0字节空间会怎么样

可以分配但不能用,尺子上的刻度,2个点才能连成线

重点:4.动态分配结构体时指针成员要单独开辟和释放空间(必须先释放成员空间再释放结构体空间

总结:动态开辟内存时结构体要为指针成员单独分配空间

结构体本身的成员是有被编译器分配内存的,给这个结构体开辟动态内存只是换了一个空间而已,但成员是指针,要明白给指针开辟内存和给指针的指向分配空间是两回事

错误例子

struct s{
    char* name;//没有初始化
}p;
int main(){
    p=(struct s*)malloc(40);
    strcpy(p->name,"lc");//没有给成员指针name的指向开辟空间,还是野指针
}

纠正:

p->name = (char*) malloc(50);

内存其他和扩展:

资源泄露

包括内存泄露和句柄泄露

句柄泄露就是关于文件之类的泄露的(还有操作系统 数据库连接 进程、线程等)。文件打开了没有关闭,创建使用完后没有销毁

防止内存耗尽:其实内存不可能耗尽。 栈虽然效率高但容量有限 。少用静态区的变量

malloc/free和new/delete区别:

malloc:不可以调用对象的构造函数,不会自动初始化,calloc可以自动初始化为0

new:会自动初始化,可以调用对象的构造函数。,delete可以释放对象的构造函数,但还是要手动设NULL


结构体:

结构规则:

把小成员集中放在前面

注意事项:普通结构体不能想减,但是可以相互赋值。结构体声明完分号不能省略

没有对象不可以初始化,除了枚举。

struct为类型标签,后面是类型定义

1.因为内存对齐不同,不能用 == 或!=比较两个结构体,只能成员和成员进行比较
2.结构体概念和作用实现数据结构

概念:不限数据类型的集合。

[^]:

3.自定义类型有哪些(结构体、枚举、联合)

结构体:
结构体定义
struct 类型名{
    成员;   //不能初始化,除了枚举,   ;分隔
}(变量)或者不写;          //分号不能省
匿名结构体定义
struct{
    成员;   //不能初始化,除了枚举,   ;分隔
}(变量)或者不写;          //分号不能省
匿名结构体概念:
匿名结构体是不写struct后面的类型名,不一定要有变量对象。匿名结构体只能一次性创建多个变量才能有多个对象,且后续不能再创建对象。如果没有对象,那只能用嵌套结构体或者用typedef改新名字,才能后续创建新的对象
自引用(包含指向自己的指针:匿名结构体做不到

错误例子:

typedef struct{
    int a;//节点
    struct s *node;//错了,还没有改名就已经提前使用了别名
}s;

正确:

typedef struct jiegou{
    int a;//节点
    struct jiegou *node;// 指向下一个节点的指针
}s;

初始化:
1.struct s 变量={像数组一样};
2.struct s 变量;
    变量.成员=xx;
计算结构体大小(内存对齐(本质。规则。用处空间换时间。改善方法)空结构体大小为1)

对齐数:类型和编译器默认对齐数比取较小值。 vs默认对齐数是8,可以用#pragma pack(4)修改默认对齐数

规则:第一个成员从偏移量为0处开始放,其他成员从自己的对齐数的整数倍开始放,最后结构体大小和成员的最大对齐数整数倍比取较大值,为结构体最终总大小

struct s{
    char s[5]; //大小为5,对齐数为1
    struct m nest;  //假设大小为6
    char c;
}; 

结构体传参用指针
结构体扩展:空结构体和匿名结构体

空结构体的作用和含义和大小(1字节char大小

6.匿名结构体突破只用一次重生之无限次使用

6.正确认识匿名·结构体,匿名结构体是不写struct后面的类型名。

2种情况:1.有对象:只能在创建结构体同时创建n个对象,后续不能再创建对象。2.无对象:那只能用嵌套结构体或者用typedef改新名字,才能后续创建新的对象
struct s {
    int id;
    struct {
        char name[50];
        char id[50];
    }; // 匿名结构体
};
s 结构体对象;
结构体对象.name  //正常访问
​
或者
typedef struct{
​
}x;           //省略了类型名,就是匿名结构体,给匿名结构体起了一个名叫x,所以x是结构体类型x arr;
//定义一个结构体类型的变量,没问题

[^]:

位段
用处(网络协议,硬件)
本质(时间换空间。打破内存对齐规则。能够灵活的操作数据存储)
定义:一般全部成员同一类型
struct stu {
    int f1 : 3; // 分配3位bit
    int f2 : 4; // 分配4位bit
};
初始化

数值超过成员的所占bit位会溢出丢弃高位,剩下的和结构体一样

为什么没有内存对齐? 因为时间换空间

位段缺点

不可移植原因 1.大小端=字节序影响内存布局,还有不同平台编译器对位段从左往右还是右往左不统一

2.不同机器整形大小不一样,32位机器,

3第二个位段成员比较大,无法容纳于第一个位段剩余的比特位时,是 舍弃剩余的位还是利用,这是不同编译器下是不确定的。

位段扩展:0位段

0位段(如果之前的位段成员没有完全填满当前的整数存储单元,编译器会跳过剩余的部分,从下一个整数单元的开始处继续放置新的位段成员。)相当于换行,目的:某些硬件要求对齐或者为了提高CPU访问效率

枚举:
作用(可读性维护,因为有实际意义,换成数值1不知道啥意思。可移植)

枚举可以移植

man//枚举常量成员假设值为1
 用man代替1更有实际意义,和宏的区别就是可调试,有类型检查,运行阶段不同
本质(不是文本替换有类型,编译阶段 语法分析枚举类型并由编译器初始化。成员类型和枚举大小就只是int)
enum e{
    max,//int类型,看编译器吧
    min
}s;
printf("%d", sizeof(s));//4  不看成员数量,就固定枚举大小为整型
定义(自动初始化从0开始后面逐项+1。成员用逗号隔开)
enum e{
    max,//第一个成员未初始化则值为0
    min=9,//虽然这是枚举常量不能被改值,但这是初始化不算赋值
    avg//逐项+1,此时为10
};
赋值(没有赋值,只有初始化,常量不能被赋值
使用(把枚举常量给枚举类型的变量

成员可以代替整型数字使用

与宏定义的区别(类型检查 调式 阶段 占用内存)

枚举占内存,宏定义不占

枚举有类型,宏定义没有

枚举在编译阶段,宏定义在预编译

计算大小(与成员数量不关,即整形大小

枚举扩展:

枚举移植性良好

[^]:

联合体:
作用(省存 不同类型转换 不同类型利用—灵活)
本质(不同类型共享内存的复合类型。存储)

所有成员共用起始地址,新赋值会更新所有成员的值,即每次只能用一个成员

联合体的值会被什么影响

假设从左往右放,大小端影响内存中的值,可能会丢失值

union u{
    int a;
    char c;
}ui;
假设从左往右放内存,假设是小端,那么假设a是1,则char类型只是一个字节,取不到内存中的1,
定义
初始化(值只用一个,被什么影响,会覆盖旧值
union u{
    int a;
    char c;
}ui;
ui.a=2;//联合体的值是2
ui.c=1;//改成1了
只能用一个值
联合体计算大小

联合体大小至少是max成员类型大小,max成员类型大小和成员最大对齐数的整数倍取较大值

对齐数:类型和编译器默认对齐数比取较小值。  vs默认对齐数是8,可以用#pragma pack(4)修改默认对齐数
union u{
    int a;//对齐数为4
    char s[5];//大小为5,对数数为1
}ui;            //最终大小:5和4的整数倍取较大值为8


文件:

1.文件的概念

文件是存储在长期存储设备(硬盘、usb驱动器)上的一系列数据的集合(视频音频文字)

缓冲区的概念

临时存放数据的内存区,(满了就自动把数据写入文件中,不需要接一滴倒一滴,像水桶)

目的:减少i/o库与文件交互次数提高效率

缓冲区分类:

全缓冲:fwrite库函数,直接清空缓冲区

行缓冲:fgets函数,遇到换行则直接清空缓冲区

无缓冲:getchar函数

标准io操作流:用来操作输入输出

stdin:输入

stdout:输出

fputs(字符数组,stdout)//输入字符数组,然后通过stdout流输出到屏幕上

stderr:错误输出

刷新缓冲区:

fflush:清空缓冲区,直接把缓冲区的东西写入内存

设置缓冲区大小:

setbuf函数

2.文件增删查改的函数使用

打开关闭:指针=fopen(),fclose(),关闭后赋NULL

读写:fgetc字符, fgets一行,fprintf(格式化%读写) ,fwrite

定位(指针指向文件的任意位置):fseek

3.文件的后缀

可执行文件:.exe .bin .base .java

目标文件:.o .obj

4.文件指针的作用

概念:file是定义在stdio.h里的一个结构体

文件指针是FILE结构体指针 ,它指向FILE结构体,该结构体包含了文件操作所需的各种信息,指向文件信息区,通过文件指针可以访问结构体中的成员

高级的i/o库(如stdio.h里的函数等)函数(如fopen,fwrite等)把文件指针作为参数实现文件操作

i/o库提供的文件指针来操作FILE结构体从而操作文件内容

也就是指针操作底层,底层调用文件操作

  • C语言的I/O库(例如stdio.h中的函数)提供了对底层文件操作的封装。这些函数通过文件指针与FILE结构体交互,调用底层系统调用(如POSIX的readwrite等)来进行实际的文件I/O操作。

5.文件定位
6.如何判断文件结束的情况

结束即到文件末尾:feof(文件指针),EOF=-1表示文件的末尾

读写失败ferror(文件指针)

两个都需要判断

7.头文件和源文件如何放置

1 .h文件包含头文件、函数的声明、宏定义、类型定义、结构体,把.h保存于include目录里,与定义文件分开 定义放文件.c里,.c保存于source目录,与声明文件分开

  1. 私有的.h和.c放同一个目录里


标准输入输出:1.scanf的理解

scanf会在缓冲区留下上一次输入的值

除了空白符换行符或者不符合格式说明符输入的数值都不会拿走。而停留在缓冲区可能导致后续读取错误,只要是正常的内容都会读取从缓冲区拿走,但由于没有给缓冲区的输入限制最大输入个数,容易缓冲区溢出

getchar相反,缓冲区有多个数值就只拿一个,不会溢出

1.结束EOF的输入用Ctrl+z或Ctrl+d
2.EOF的正确使用类型是int

getchar返回值可能是EOF(-1),所以要用int来接收,char不在-1范围会出问题!

int ch=0;
while(ch=getchar(ch)!=EOF){} 或者while(~ch=getchar(ch)){}

输出:

让printf打印的宽度自定义
int x = 2;
printf("%*d",width, x);   //*号不能省,用来越过width参数
double y = 3.14159;
printf("%.*f", 2, y);//保留2位小数
printf("%*f", width, y);


预处理:

1.程序运行编译的过程
概括:

多个.c文件经过编译环境,在通过连接器把多个目标文件合成可执行的一个目标文件,在由操作系统放入内存中找main开始运行

详细:

第一步:.c被预处理器译成.i文件,处理所有的预处理指令(带#号的)和注释。

第二步:.i文件被编译器译成.s汇编文件,进行语义语法词法分析和符号汇总,以及源代码优化和目标代码优化。

汇编阶段:.s文件被汇编器译成可重定位的.o目标文件,再把汇总好的符号表交给链接器生成符号表(符号和符号对应的地址:max,0x11)

第四部链接阶段:1.进行合并段表即把每个目标文件的的段分类并合成到一个目标文件中并按该目标文件的内存布局分配内存,形成一个可执行的目标文件 2.重定位目标文件:链接器会在合并段表的过程中解析符号,如果有未定义的符号,则再从链接库(标准库、自定义函数、第三方库等)找符号,找到后替换其地址为合并后的该目标文件的相对地址,否则链接错误报错(包括重复定义符号),

最后把目标文件合成单一的输出文件.exe

运行阶段:

操作系统把程序放入内存中,打开io库和网络连接,打开三个区(静态区、栈区堆区)并把文件里的符号的虚拟地址映射到三个区的物理内存中,开始找main直至结束或断电。

指令
预处理宏:

不可修改,在预处理阶段已经替换好了值,如:显示编译日期,显示这个宏在哪行执行,显示当前源文件的相对地址等

格式是:_ _ xxx_ _

3.宏(预处理宏和宏定义区别

本质是文本替换,不做语法分析

用法:#define 名(可以是函数) 内容(内容可以空格),后面不要加;号,因为算作内容

撤销宏定义:

#define PI 3.14159
#undef PI
    // 重新定义 PI
#define PI 3.14
宏不进行语法解析:
#define PTR int*
PTR a, b; //实际上a是int*类型,而b是int类型,违反了本意是定义两个指针的愿望
typedef做语法解析:
typedef int* IntPtr;
IntPtr a, b;//两个都是指针类型

宏规则:

1.符号优先级易错,所以必须每个符号加括号

2.不要用自增用+1

3.名字要全大写,常量的命名规则

#和##的作用

#:把宏转成字符串

#define f(x) printf(#x)    f(a)则打印a相当于printf("a");
#define f(x) printf("#x")这样就没用了,相当于printf函数了,直接打印#x,传不了参数

##:粘在一起

a##b  等价于ab

缺点:.不可调试,可读性低,纯文本替换可能和人的愿望不符

不进行语法解析:

优点:不占内存,可以传类型:

#define f(x) printf("%d",sizeof(x))
f(double);   //8
总结和函数到底用哪个:

除了运算等小规模的或者需要作函数传类型的用宏,其他用函数

4.条件编译

只能包含常量、宏等预处理阶段执行的东西,选择性编译代码

#if 条件(常量表达式)
//代码
#endif

5.包围守卫

2种方法结合:#pragma once不是标准委员会定的标准,可能移植性不好,宏定义是标准委员会规定的,但不受广泛使用

#pragma once    
​
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
​
// 头文件声明
​
#endif // HEADER_FILE_NAME_H

有用的库函数:

1.生成随机数 rand()和srand()

 #include<stdlib.h>
  #include<time.h>
   int main() {
    // 使用当前时间作为种子值初始化随机数生成器
    srand(time(NULL));  //先写这个,如果不写随机数可能不理想
​
    // 生成一个介于 a 和 b 之间的随机数
    int 变量 = rand() % (b - a + 1) + a;//因为不包括,所以+1不能取等。最小值是a开始
    printf("随机数 (%d-%d): %d\n", a, b, 变量名);
​
    return 0;
}

2.错误码函数error

3.断言

让printf打印的宽度自定义
int x = 2;
printf("%*d",width, x);   //*号不能省,用来越过width参数
double y = 3.14159;
printf("%.*f", 2, y);//保留2位小数
printf("%*f", width, y);

4.xx类型转成xx类型的函数

数字转字符串:通过sprintf把xx类型的数据转成xx类型的数据,不过试过容易崩溃一不小心加了空格

sprintf(字符数组, "%d", 123); //把123转成“123”     字符串为什么不能直接写“abc”呢?因为常量不能改变
printf("%s", 字符数组); //sprintf不会自动打印
​扩展:
    sprintf(要转成某类型的数据, "源头的格式说明符", 源头某类型的数据);

5.math.h可以使用数学公式如sin,cos等等


编程规范:

1.非常实用,以后都要用

为什么有些人写成if(NULL==p) 而不是if(p==NULL) ?
因为可以防止把==写错成=时让编译器可以报错,减少错误

提高全局效率为主,局部效率为辅;

在可读性,健壮性,可靠性前提下在提高效率优化内存

优化内存时,不要在小事情上优化,要找出限制效率的关键

先优化数据结构和算法,在优化代码

紧凑的代码不要追求

不要发明已经存在的库函数,尽量使用标准库函数

不要用非常灵活的数据结构(丧失别的东西。非常灵活的数据结构常常需要在存储空间和时间复杂度上进行更 多的妥协。)


杂项

1.为什么有动态链接?和静态链接的区别?
2.烫的由来
3.malloc怎么申请到内存的
4.头文件包含的功能
其他问题:

1.循环体只循环了一次: 因为在循环体后面加了分号

2.程序执行到main第一个语句就挂了:因为栈爆满了栈溢出,递归调用过深或局部变量占用过多内存导致的

C语言实现面向对象:

实现抽象数据类型(定义接口实现)

如何生成只能被部分源文件看见的变量(类似受保护的),C语言不行

C语言的auto毫无用途

专业名词:

域:函数{}里的块域,变量域,标识符,文件域的有效范围就叫域

  • 29
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全评估测试题大模型安全评估测试题关键词库生成内容测试题库应拒答测试题库非拒答测试题大模型安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值