> 文章列表 > 补充C语言

补充C语言

补充C语言

1.关键字

前言: 

  • C90一共有32个关键字,
  • C99比C90多了5个关键字,但主流的编译器对C99关键字支持的不是特别好,
    • 所以后面主要以C90的32个关键字为标准

 1.1认识auto关键字

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 
int main()
{int i = 0;auto int j = 0;return 0;
}
  • 一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略

1.2认识register关键字 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
int main()
{register int a = 0;printf("&a = %p\\n", &a);return 0;
}

  • register会尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的
  • 现在的编译器已经很智能化了,它能够自主的决断是否将变量放入CPU寄存器
  • 被register修饰的变量,不能取地址
    • 因为已经放在寄存区中了,地址是内存相关的概念

 1.3认识extern关键字

  •  被extern修饰的变量或者函数表示为声明外部属性
  • 注意: extern int g_val = 11;是错误的,extern只能声明,不能定义,初始化,赋值等等 
  •  编程好习惯: 声明变量或函数的时候,都带上extern
    •  比如: extern int g_val = 100;extern void show();

1.4认识static关键字 

  •  被static修饰的全局变量或函数,只能在本文件内被访问,不能被外部其他文件直接访问
  •  被static修饰的局部变量,会更改局部变量的生命周期,将其放在静态区
  •  编码好习惯:函数名的首字母大写, 

    • 比如:函数名My_strlen

1.5细节sizeof关键字

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int a = 10;printf("%d ", sizeof(a));printf("%d ", sizeof a);printf("%d ", sizeof(10));printf("%d ", sizeof 10);printf("%d ", sizeof(int));// printf("%d ", sizeof int);return 0;
}
  •  sizeof + 变量或常量,有无括号都行,但是不能计算变量类型

  •  编程好习惯: 在定义全局变量或函数的时候,名字应表示为g_名字
    • 比如:int g_val = 10;void g_show()

1.6认识bool(_Bool)关键字

 C99引入了_Bool类型(你没有看错,_Bool就是一个类型,不过在新增头文件stdbool.h中,被重新用宏写成了bool,为了保证C/C++兼容性)。

  • bool用宏重新封装了_Bool

 编程好习惯

  •  左边的代码风格 明显 比 右边的代码风格 更加优秀
  • 右边的代码会让人产生误解,认为是在判断if条件中的flag是不是等于零

1.7细节double关键字

double类型 变量与"零值"进行比较

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{double x = 1.0;double y = 0.1;printf("%.50lf\\n", x);printf("%.50lf\\n", y);if ((x - 0.9) == y) {printf("you can see me!\\n");}else {printf("oops!\\n");}return 0;
}

  • 0.1在double类型中存储的其实会比0.1大一点点
    • 因为存储的时候出现了精度损失 

 如果double中的0.1实际的0.1的绝对值小于DBL_EPSILON,就认为它几乎等于实际的0.1

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<float.h>
#include<math.h>
int main()
{double x = 1.0;double y = 0.1;printf("%.50lf\\n", x);printf("%.50lf\\n", y);if (fabs((x - 0.9) - y)< DBL_EPSILON) {printf("you can see me!\\n");}else {printf("oops!\\n");}return 0;
}

  •  这段代码中的fabs是求double类型绝对值的函数,abs是求int类型绝对值的函数
  • fabs((x - 0.9) - y)< DBL_EPSILON,注意这里不能写等于,

1.8细节switch,case关键字

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>int main()
{int num = 1;int b = 1;switch (num){case 1:// int a = 1;// errorbreak;case 2:b = 1;break;case 3:{int c = 1;}break;default:break;}return 0;
}
  • case中不能定义变量,如果要在case中定义变量必须加上代码块{}

  • 编程好习惯:case匹配时,尽量把常见的情况放在前面

1.9细节continue关键字

  •  在for循环中continue是跳到改变循环变量的位置
  • 编程好习惯: 双层for循环的时候,尽量保证外小内大(范围)

1.10 细节void关键字

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>int main()
{// void a = 0;// 不允许使用不完整的类型printf("%d", sizeof(void));return 0;
}
  •  void本身就被编译器解释为空类型,强制的不允许定义变量
  •  在linux中void的大小是1,而在vs2019中void的大小是0 

1.11细节return关键字 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>
int SumAdd() {int sum = 0;for (int i = 1; i <= 10; i++) {sum += i;}return sum;
}
int main()
{int sum = SumAdd();printf("%d", sum);return 0;
}

  •  return返回的时候,会通过寄存器的方式,返回给函数调用方,及时没有接收也一样 

1.12细节const关键字

  •  在C语言中,const机制是通过编译器检查实现的,它标记const变量不能被直接修改,但并未限制const变量的地址的引用,
  • 只要变量的地址存在被引用可能,就说明该变量是可以通过指针被间接修改的。
  • 所以,只要我们能保证程序在编译过程不出错,那么在程序的运行过程中我们便可以通过指针间接修改该const修饰的变量的值

1.13细节struct关键字

  •   空结构体的大小,在不同的编译器下是不同的

 1.14细节typedef关键字

  • 存储关键字有:auto,extern,register,static,typedef
  • 存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个 深入理解变量的左右值

  •  左值表示空间,右值表示内容
  • 任何一个变量名,在不同的应用场景中,代表不同的含义

深入理解变量内容的存入和取出

signed int b = -10;

-10存入:

  • -10的原码:1000 0000 0000 0000 0000 0000 0000 1010
  • -10的补码:1111 1111 1111 1111 1111 1111 1111 0101
  • -10的补码:1111 1111 1111 1111 1111 1111 1111 0110

-10取出:(有符号,需要再转换一下)

  •  1000 0000 0000 0000 0000 0000 0000 1010

unsigned int d = -10; 

-10存入:

  • -10的原码:1000 0000 0000 0000 0000 0000 0000 1010
  • -10的补码:1111 1111 1111 1111 1111 1111 1111 0101
  • -10的补码:1111 1111 1111 1111 1111 1111 1111 0110

-10取出:

  •  1111 1111 1111 1111 1111 1111 1111 0110

案例

 

  • 关键在于到底是%d打印,还是%u打印(数据究竟是以有符号还是无符号的形式取出的)

总结:

  1. 存:字面数据必须先转成补码,再放入空间当中,所谓符号位,完全看数据本身是否携带+-         号。和变量是否有符号无关!
  2. 取:数据一定要先看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二         进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪         里,又要明确大小端)

深入理解char类型中的-128 

-128的原码:1 1000 0000

-128的反码:1 0111 1111

-128的补码:1 1000 0000

char类型只有8个bit位,所以-128存入char中的时候会发生截断

  • -128在char中为1000 0000,他们规定这个1000 0000就当作-128
  • 注意1000 0000和0000 0000都可以表示0

 数据类型的取值范围:-2^(n-1)到2^(n-1)-1

  • 上面这个公式中的n表示的是数据类型的bit位 
  • 比如char取值范围:[-2^7,2^7-1] 就是 [-128,127]
  • 比如short取值范围:[-2^15,2^15-1] 
  • 比如int取值范围:[-2^31,2^31-1] 

2.符号

 ++和--

  • 不管是前置++,还是后置++,都是通过寄存器来改变值的,
  • 注意: 在没有接收方的时候,前置++和后置++是一样的

 深度理解取余/取模运算

2.1取整运算

  1. 四舍五入取整(round)
  2. 向负无穷取整(floor)
  3. 向正无穷取整(ceil)
  4. 向0取整(trunc)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
#include <math.h> 
int main() 
{ const char * format = "%.1f \\t%.1f \\t%.1f \\t%.1f \\t%.1f\\n"; printf("value\\tround\\tfloor\\tceil\\ttrunc\\n"); printf("-----\\t-----\\t-----\\t----\\t-----\\n"); printf(format, 2.3, round(2.3), floor(2.3), ceil(2.3), trunc(2.3));printf(format, 3.8, round(3.8), floor(3.8), ceil(3.8), trunc(3.8));printf(format, 5.5, round(5.5), floor(5.5), ceil(5.5), trunc(5.5));printf(format, -2.3, round(-2.3), floor(-2.3), ceil(-2.3), trunc(-2.3));printf(format, -3.8, round(-3.8), floor(-3.8), ceil(-3.8), trunc(-3.8));printf(format, -5.5, round(-5.5), floor(-5.5), ceil(-5.5), trunc(-5.5)); return 0;
}

 

  • 在vs2019中的取整规则是向0取整 

 2.2负数取模

取模定义 

 如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r

满足 a = q*d + r , q 为整数,且0 ≤ |r| < |d|其中,q 被称为商,r 被称为余数。

  • 在C语言中:-10=(-3)*3+(-1),因为C语言中是向0取整所以商是-3,余数是-1,也叫负余数
  • 在Python中: -10=(-4)*3+2,因为Python中是向负无穷取整所以商是-4,余数是2,也叫正余数
  • 所以,在不同语言,同一个计算表达式,负数“取模”结果是不同的

2.3取余和取模的关系 

取余本质:尽可能让商,进行向0取整

取模本质:尽可能让商,向负无穷方向取整

对任何一个大于0的数,对其进行0向取整和-∞取整,取整方向是一致的。故取模等价于取余

对任何一个小于0的数,对其进行0向取整和-∞取整,取整方向是相反的。故取模不等价于取余

  • C中%,本质其实是取余
  • Python中%,本质其实是取模
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h> 
int main() 
{ printf("%d\\n", -10 / 3); //结果:-3 printf("%d\\n\\n", -10 % 3); //结果:-1 为什么? -10=(-3)*3+(-1) printf("%d\\n", 10 / -3); //结果:-3 printf("%d\\n\\n", 10 % -3); //结果:1 为什么?10=(-3)*(-3)+1 return 0; 
}

 

  •  明显结论:如果不同符号,余数的求法,参考之前定义。而余数符号,与被除数相同

3.预处理过程

3.1宏定义充当注释符号 

  •  由上面的结果可以得到: 预处理期间先执行去注释,然后再进行宏替换

3.2宏定义替换多条语句

  •  为了解决else匹配的问题,这里引入了do-while-zero结构

3.3宏定义的实际范围

  •  在源文件的任何地方都可以定义宏,有效范围是从定义处向下都有效
  • #undef: 是取消宏定义 

3.4详细条件编译 

 

  •  #ifdef 宏: 定义了,就执行
  •  #ifndef 宏: 没定义了,就执行

多条件下使用条件编译

  •  #if #elif #else #endif 来解决多条件的情况
  • 补充: #if defined等价于#ifdef,#if !defined等价于#ifndef 

3.5头文件的展开

  •  头文件的展开就是拷贝库函数到当前文件
  • 使用#ifndef #pragma once都可以解决头文件被重复包含的问题

3.6了解#error 预处理

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
//#define __cplusplus 
int main() 
{#ifndef __cplusplus #error 老铁,你用的不是C++的编译器哦 #endifreturn 0; 
}

  •  #error可以自定义编译报错

 3.7 了解 #line 预处理

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
int main() 
{ //C预定义符号,代表当前文件名和代码行号 printf("%s, %d\\n", __FILE__, __LINE__); 
#line 60 "hehe.h" //定制化完成printf("%s, %d\\n", __FILE__, __LINE__); return 0; 
}
  • #line可以 定制化文件名称代码行号

3.8 # 运算符 

  •  将参数符号s对应的文本内容,转换成为"字符串"

3.9  ## 预算符

  • 3.14e3是指数的科学计数法,这个运算符会将##相连的符号和合成一个新的字符,

4.指针和数组 

4.1 理解指针和指针变量

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int* p = NULL;p = (int*)0x1234;int* q = p;return 0;
}
  • 指针变量:空间(左值)+内容(右值:地址)
  • 第一个p使用的是变量p的空间
  • 第二个p使用的是变量p的内容,也就是0x1234,此时指针==指针变量 

4.2 理解指针变量的解引用

  • 0x1234如果是赋给p指针变量的空间是不会报错的,
  • 由此推断出0x1234是赋给p指针变量的内容,
  • 指针变量进行解引用,使用的是指针变量的右值(内容)

 4.3 了解变量地址

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int a = 10;printf("%p\\n", &a);return 0;
}

  •  栈随机化技术: 使得每次重新编译打印的时候都地址都不一样

 4.4 数组内存布局

  • 局部变量都是在栈区上面的,而栈区的使用习惯是先使用高地址再使用低地址
  • 在开辟空间的角度,不应该把数组认为成一个个独立的元素,要整体开辟,整体释放

 4.5理解数组传参

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void show(int pr[])
{int i = 0;for (i = 0;i < 10;i++){printf("%d ", *(pr + i));//printf("%d ",pr[i]);}
}
int main() 
{ int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;show(arr);return 0; 
}
  • 为了解决拷贝问题, 所有的数组,传参都会发生降维,都会降维指向内部元素类型的指针! 

4.6 理解指针和数组访问元素的相似性

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void show(int* pr)
{int i = 0;for (i = 0;i < 10;i++){printf("%d ", *(pr + i));//printf("%d ",pr[i]);}printf("\\n");
}
int main() 
{ int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;show(arr);for (i = 0;i < 10;i++) {printf("%d ", arr[i]);//printf("%d ", *(arr + i));}return 0; 
}
  •  虽然指针和数组都可以通过*和[] 进行解引用,但他们的寻址方案是完全不一样的
  • 这样设计就可以减低编程的难度,不用来回切换

4.7 了解数组定义 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{int a = 0;float b = 0.0f;int arr[10] = { 0 };//int [10] arr = { 0 };return 0;
}
  •  在C语言中数组的定义: int arr[10]
  • 在C#语言中数组的定义: int [10] arr

4.8 理解数组元素 

  • 数组里的元素是数组类型的一部分 

 4.9 数组经典题 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{int a[4] = { 1,2,3,4 };int* ptr1 = (int*)(&a + 1);int* ptr2 = (int*)((int)a + 1);printf("%x,%x\\n", ptr1[-1], *ptr2);return 0;
}

 

 

  • 大多数机器都是小端机,存的时候用小端,取的时候也用小端 

4.10 了解多维数组结构

  • 所有的数组都可以当成"一维数组",多维数组就相当于一维数组不停的套娃 

5. 内存管理

5.1 验证C程序动态地址空间分布

5.2详谈内存越界问题

案例一 

  •  越界不一定报错

案例二 

  •  对于数值越界访问的检查,是一种抽查机制

案例三

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
#include <stdlib.h>
int main()
{while (1) {int* p = malloc(1024);}return 0;
}
  • 程序退出,内存泄漏问题就不在了,被自动回收了
  • 内存泄漏问题对于那些永远不会主动退出的程序,比如:操作系统,杀毒软件,服务器等,影响大、

5.3 C中动态内存管理体现

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
#include <stdlib.h>
int main()
{char* p = (char*)malloc(sizeof(char) * 10);printf("before:%p\\n",p);free(p);printf("after:%p\\n", p);return 0;
}

  • 其实释放的字节会比实际上10个字节多得多,申请的一定不止10字节
  • malloc申请空间的时候,系统给你的其实更多,而多出来的那部分,记录了这次申请的更详细信息,
  • free的释放,相当于取消关系,使之后的p无法再使用

 6. 函数栈帧

6.1 样例代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
int MyAdd(int a, int b) 
{ int c = a + b;return c; 
}
int main() 
{ int x = 0xA;int y = 0xB; int z = MyAdd(x, y);printf("z = %x\\n", z);return 0; 
}

6.2认识相关寄存器

寄存器名称                                  作用

eax

通用寄存器,保留临时数据,常用于返回值
ebx 通用寄存器,保留临时数据
ebp 栈底寄存器
esp 栈顶寄存器
eip 指令寄存器,保存当前指令的下一条指令的地址

 6.3认识相关汇编命令

汇编命令                                  作用
mov 数据转移指令
push 数据入栈,同时esp栈顶寄存器也要发生改变
pop 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub 减法命令
add 加法命令
call 函数调用,1. 压入返回地址 2. 转入目标函数
jump 通过修改eip,转入目标函数,进行调用
ret 恢复返回地址,压入eip,类似pop eip命令

step1: main函数也是要被调用的 

  •  其实main()函数是在_tmainCRTStartup函数中调用的,
    • 创建main( )函数的栈帧,
    • 完成状态寄存器的保存,
    • 堆栈寄存器的保存,
    • 函数内存空间的初始化。

  step2:main函数栈帧创建

  • 寄存器ebp指向当前的栈帧的底部高地址
  • 寄存器esp指向当前的栈帧的顶部(低地址)
  • 第一张图片的那2条mov汇编是在main函数的栈帧中,开辟变量x和变量y,并赋值
  • 第二张图片的那4条汇编做了一件事->形参实例化(且形参实例化是向从最右边开始实例化的) 

step3:调用Myadd函数->压栈

  •  call这条汇编主要做 1. 压入返回地址 2. 转入目标函数
  • 压入返回地址b0 53 8f 00是为了以后能找到

step4:创建Myadd函数的栈帧->入栈

  1. 执行push汇编命令,esp的指向会变
  2. 执行mov汇编命令
  3. 执行sub汇编命令(开辟空间的大小和里面的代码有关 )

step5:释放Myadd函数的栈帧->弹栈

  • mov汇编命令: 就可以说Myadd的函数被释放了
  • pop汇编命令: 会把ebp指向main函数的栈底,esp也会变
  • ret汇编命令: 会把b0 53 8f 00写回eip中

step6: Myadd函数结果返回 

  •  add汇编命令: 会把esp+8
  • add汇编命令: 会得到eax中的值
  •  Myadd函数的返回值是通过寄存器来返回的

总结:

  1. 调用函数,需要先形成临时拷贝,形成过程是从右向左的

  2.  临时空间的开辟,是在对应函数栈帧内部开辟的

  3. 函数调用完毕,栈帧结构被释放掉

  4.  临时变量具有临时性的本质:栈帧具有临时性

  5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本

  6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的

 编程好习惯

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
int MyAdd(const int* a, const int* b)
{if (a == NULL || b == NULL) {printf("NULL error\\n");return 0;}int c = *a + *b;// 尽量用空格缩进return c;
}int MySub(const int* a, const int* b)
{if (a == NULL || b == NULL) {printf("NULL error\\n");return 0;}int c = *a + *b;// 尽量用空格缩进return c;
}int main()
{int num1 = 1, num2 = 2;// MyAdd(NULL, NULL);MyAdd(&num1, &num2);MySub(&num1, &num2);return 0;
}
  • 函数与函数之间空一行
  • 如果参数是输入型的指针,则应在类型前加const以防止该指针在函数体内被意外修改
  • 缩进的时候尽量用空格缩进,如果用tab的话可能会发生排版的乱序 
  • assert检测空指针的时候,只会在Debug下才有效,所以建议用if判断空指针的情况

7.可变参数列表

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h> 
#include <stdarg.h>int FindMax(int num,...)
{va_list arg;//定义可以访问可变参数部分的变量,其实是一个char*类型va_start(arg, num);//使arg指向可变参数部分int max = va_arg(arg, int);//根据类型,获取可变参数列表中的第一个数据int i = 0;for (i = 0; i < num - 1; i++) {int curr = va_arg(arg, int);if (max < curr) {max = curr;}}va_end(arg);return max;
}int main()
{int max = FindMax(5, 11, 22, 33, 44, 55);printf("max=%d\\n", max);return 0;
}

  • 参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start
  • 如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。 
  • 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
  • 实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行提升
  • 函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行

7.1可变参数列表原理

va_list 和 va_end 

  •   va_list : 定义可以访问可变参数部分的变量,其实是一个char*类型
  •  va_end : 相当于把arg指针置成空

va_start 

  •  va_start : 使arg指向可变参数部分

va_arg 

 

  •  va_arg: 根据类型,获取可变参数列表中的第一个数据
  • 这里的arg指针减去4字节,再加上4字节,可以说设计的非常巧妙 

 #define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

  • 是一个求最小对齐数的宏,这是4的倍数

理解一:4的倍数

  • 既然是4的最小整数倍取整,那么本质是:x=4*m,
  • m是具体几倍对7来讲,m就是2,对齐的结果就是8,而m具体是多少,取决于n是多少
  • 如果n能整除4,那么m就是n/4,如果n不能整除4,那么m就是n/4+1
  • 由此产生了一种写法:4的倍数等于(n+3)/4,也就是( n+sizeof(int)-1) )/sizeof(int)

理解二:最小4字节对齐数

  • 搞清楚了满足条件最小是几倍问题,那么,计算一个最小数字x,满足 x>=n && x%4==0,
  • 就变成了 4字节对齐数等于((n+4-1)/4)*4 也就是((n+sizeof(int)-1)/sizeof(int))[最小几倍] * sizeof(int)[单位大小]

理解三:理解源代码中的宏

  • ((n+4-1)/4)* 4,设w=n+4-1,表达式就变成了(w/4)*4,
  • 其中一个数除4等价于二级制位右移2位,一个数乘4等价于二级制位左移2位
  • 简洁版:(n+4-1) & ~(4-1)
  • 原码版:( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),

 8.简单了解命令行参数

  • main函数也是可以传参的,
  • 第一个参数: argc 是个整型变量,表示命令行参数的个数(含第一个参数)。
  • 第二个参数: argv 是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字符串)。
  • argv数组的最后一个元素存放了一个 NULL 的指针。

谷姚养殖网