【C语言】零碎知识点(易忘 / 易错)总结回顾

2024-09-04 21:04

本文主要是介绍【C语言】零碎知识点(易忘 / 易错)总结回顾,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、数据类型

1、%p —— 以地址的形式打印


2、整型在内存中的存储

(1)原码、反码、补码

计算机中的有符号数有三种表示方法:原码、反码和补码。

三种表示方法均有符号位和数值位两部分,数值位三种表示方法各不相同。

  • 原码:直接将二进制按照正负数的形式翻译成二进制就可以了。
  • 反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
  • 补码:反码 +1 就得到补码。

补码转原码

  1. 方法 1:先 -1,再符号位不变,按位取反。
  2. 方法 2:将原码到补码的过程再来一遍。

对于整型来说:数据存放内存中其实存放的是补码(因为使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU 只有加法器),且补码与原码相互转换的运算过程是相同的,不需要额外的硬件电路)。


(2)大小端

  • 大端(存储)模式:数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中。
  • 小端(存储)模式:数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

一个 16bit 的 short 型 x ,在内存中的地址为 0x0010,x 的值为 0x1122 ,那么 0x11 为高字节(数据的高位), 0x22 为低字节(数据的低位)。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中,0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。常用的 X86 结构是小端模式。


3、浮点型在内存中的存储

根据国际标准 IEEE(电气和电子工程协会)754,任意一个二进制浮点数 V 可以表示成的形式:(-1)^S * M * 2^E

  • (-1)^S 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。
  • M 表示有效数字,其值大于等于 1,小于 2。
  • 2^E 表示指数位。

十进制的 5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 按照 V 的格式,可以得出 S=0,M=1.01,E=2。十进制的 -5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。

IEEE 754 规定: 对于 32 位的浮点数,最高的 1 位是符号位 S,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M。

存入内存时,E 的真实值必须再加上一个中间数,对于 8 位的 E,这个中间数是 127;对于 11 位的 E,这个中间数是 1023。


4、char

char 类型通常是一个 8 位的有符号整数(signed char),其取值范围是 -128 到 127。如果下面这段代码中的 -1 - i 小于 -128,它会回绕到正值。strlen 函数计算的是以 '\0'(空字符)结尾的字符串的长度。a 数组的每个元素都被赋值为 -1 - i,由于 -1 - i 很快就会小于 -128,从而溢出到正值 127,再到 0。也就是 -1~-128 + 127~1,一共 255 个数字。

unsigned char 通常占用 8 位,允许的值范围是从 0~255。增加到 256 就超过了 unsigned char 的最大值,当达到 255 后,下一个值实际上是 256,二进制表示 00000000 00000000 00000001 00000000 被裁剪到 8 位,即 00000000 就是 0,进入死循环。


二、分支语句

在 C 语言中,0 表示假,非 0 表示真(不是 1,而是非 0)。


三、goto 语句

最常见的用法就是终止程序在某些深度嵌套的结构的处理过程,例如一次跳出两层或多层循环。


四、形式参数(形参)

只有在函数被调用的过程才会进行实例化(分配内存单元),形式参数当函数调用完成之后就会自动销毁。

形参实例化之后其实相当于实参的一份临时拷贝。


五、函数的链式访问

把一个函数的返回值作为另外一个函数的参数。


六、库函数

1、字符串函数

(1)strlen(求字符串长度)

计算字符串长度,直到遇到 '\0'。

cplusplus.com/reference/cstring/strlen/?kw=strlen

返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0')。

参数指向的字符串必须要以 '\0' 结束。

注意函数的返回值为 size_t,是无符号的。

模拟实现的三种方法:


(2)strcpy(长度不受限制的字符串函数)

字符串拷贝函数,将源字符串的内容拷贝到目标空间内

http://www.cplusplus.com/reference/cstring/strcpy/

会将源字符串中的 '\0' 拷贝到目标空间。

目标空间必须足够大,以确保能存放源字符串。

目标空间必须可变。

模拟实现:


(3)strcat(长度不受限制的字符串函数)

字符串追加(连接)函数,将一个字符串的内容追加到另一个字符串结束的末尾

cplusplus.com/reference/cstring/strcat/

目标空间必须足够大,能容纳下源字符串的内容。

目标空间必须可修改。

不能给自己追加。

模拟实现的两种方法:


(4)strcmp(长度不受限制的字符串函数)

字符串比较函数

cplusplus.com/reference/cstring/strcmp/

标准规定

  • 第一个字符串大于第二个字符串,则返回大于 0 的数字。
  • 第一个字符串等于第二个字符串,则返回 0。
  • 第一个字符串小于第二个字符串,则返回小于 0 的数字。

模拟实现:


(5)strncpy(长度受限制的字符串函数)

拷贝 num 个字符从源字符串到目标空间。

cplusplus.com/reference/cstring/strncpy/

如果源字符串的长度小于 num,则拷贝完源字符串之后,在目标的后边追加 0,直到 num 个。


(6)strncat(长度受限制的字符串函数)

cplusplus.com/reference/cstring/strncat/


(7)strncmp(长度受限制的字符串函数)

cplusplus.com/reference/cstring/strncmp/

比较到出现一个字符不一样或者一个字符串结束或者 num 个字符全部比较完。


(8)字符串查找

A. strtok

cplusplus.com/reference/cstring/strtok/

delimiters 参数是个字符串,定义了用作分隔符的字符集合。

第一个参数指定一个字符串,它包含了 0 个或者多个由 delimiters 字符串中一个或者多个分隔符分割的标记。

strtok 函数找到 str 中的下一个标记,并将其用 '\0' 结尾,返回一个指向这个标记的指针。
(注:strtok 函数会改变被操作的字符串,所以在使用 strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改)

  • strtok 函数的第一个参数不为 NULL ,函数将找到 str 中第一个标记,strtok 函数将保存它在字符串中的位置。
  • strtok 函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

B. strstr

在字符串 str1 中查找第一次出现字符串 str2 的位置。

cplusplus.com/reference/cstring/strstr/

模拟实现的两种方法:

也可以使用 strncmp 函数直接比较字符串,这个函数和 strcmp 函数功能一致,但它可以控制比较几个字符。


(9)错误信息报告

A. sterror

返回错误码所对应的错误信息。

cplusplus.com/reference/cstring/strerror/?kw=strerror


2、内存操作函数

(1)memcpy

从 src 的位置开始向后复制 num 个字节的数据到 dest 的内存位置。

cplusplus.com/reference/cstring/memcpy/?kw=memcpy

遇到 '\0' 的时候并不会停下来。

如果 src 和 dest 有任何的重叠,复制的结果都是未定义的。

模拟实现:


(2)memmove

cplusplus.com/reference/cstring/memmove/

和 memcpy 的差别:memmove 函数处理的源内存块和目标内存块是可以重叠的。

模拟实现:


(3)memcmp

比较从 ptr1 和 ptr2 指针开始的 num 个字节。

cplusplus.com/reference/cstring/memcmp/


3、字符函数

(1)isdigit

判断是否为十进制数字 0~9。

  • cplusplus.com/reference/cctype/isdigit/

(2)islower

判断是否为小写字母 a~z。

  • cplusplus.com/reference/cctype/islower/

(3)isupper

判断是否为大写字母 A~Z。

  • cplusplus.com/reference/cctype/isupper/

(4)isalpha

判断是否为字母 a~z、A~Z。

  • cplusplus.com/reference/cctype/isalpha/

(5)isalnum

判断是否为字母或者数字 a~z、A~Z、0~9。

  • cplusplus.com/reference/cctype/isalnum/

(6)字符转换

  • char tolower(char c); 转化为小写字母。
  • char toupper(char c); 转化为大写字母。

4、数学函数

(1)trunc

取整函数,跟除法操作结果一致。

  • cplusplus.com/reference/cmath/trunc/

(2)floor

本质都是向 -∞ 取整。

  • cplusplus.com/reference/cmath/floor/

(3)ceil:本质都是向 +∞ 取整。

  • cplusplus.com/reference/cmath/ceil/

(4)round:本质是四舍五入。

  • cplusplus.com/reference/cmath/round/

七、数组

数组创建,[ ] 中要给一个常量才可以,不能使用变量。

先定义的变量,地址是比较大的,后续依次减小。

数组是整体申请空间的,然后将地址最低的空间,作为 a[0] 元素,依次类推。

数组在创建的时候如果不指定数组的确定的大小就必须进行初始化。

对指针 +1,本质上是加上其所指向类型的大小。

数组名不可以做左值。能够充当左值的,必须是有空间且可被修改的,arr 不可以整体使用,只能按照元素为单位进行使用。

数组在内存中是连续存放的(二维数组在内存中也是连续存储的)。

数组名是数组首元素的地址。(有两个例外,它们的数组名表示整个数组)

  1. sizeof(数组名):计算整个数组的大小,sizeof 内部单独放一个数组名,数组名表示整个数组。
  2. &数组名:取出的是数组的地址。&数组名,数组名表示整个数组。

这样会出问题,调试之后可以看到 bubble_sort 函数内部的 sz 是 1(函数内部的 sizeof(arr) 结果是 4,sizeof(arr[0]) 的结果也是 4)。当数组传参时,实际上只是把数组首元素的地址传递过去了。sizeof(arr) 将返回指针的大小,而不是数组的实际大小。

正确写法:


1、数组参数

(1)二维数组传参

函数形参的设计只能省略第一个 [] 的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行有多少个元素,这样才方便运算。(例如:int arr[][5])


(2)函数指针数组

把函数的地址存到一个数组中,那这个数组就叫作函数指针数组。

函数指针的定义 int (*parr1[10]])():parr1 先和 [ ] 结合,说明 parr1 是数组,数组的内容是 int (*)() 类型的函数指针。

用途:转移表。

int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; 定义函数指针数组,作转移表。(add, sub, mul, div 都是有 2 个 int 参数的函数,返回类型也是 int)使用方式:ret = (*p[input])(x, y);


八、操作符

1、算术操作符

对于 / 操作符来说,如果两个操作数都为整数,执行整数除法,而只要有一个操作数是浮点数,那么执行的就是浮点数除法。

  • 向 0 取整:其实就是直接去掉小数点后的部分。

% 的操作符的两个操作数必须为整数。返回的是整除之后的余数。

在不同的语言中,同一个计算表达式,负数 “取模” 的结果是不同的。

  • C 中的 %,本质其实是取余。
  • Python 中的 %,本质其实是取模。

取余:尽可能让商向 0 取整。余数符号与被除数相同(C/C++、Java)。

取模:尽可能让商向 -∞ 方向取整。

参与取余的两个数据,如果同符号,取模等价于取余。

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


2、移位操作符

(1)<< 左移操作符

左边抛弃、右边补 0


(2)>> 右移操作符

  1. 逻辑移位:左边用 0 填充,右边丢弃
  2. 算术移位:左边用原该值的符号位填充,右边丢弃

先判定是算术右移还是逻辑右移,判定依据:看自身类型,和变量的内容无关。

移位运算符的优先级低于 + -

对于移位运算符,不要移动负数位,这个是标准未定义的

错误写法:


3、位操作符

&:按位与、|:按位或、^:按位异或

它们的操作数必须都是整数

^ 异或运算符的特性

  • 自反性:x ^ x = 0(任何数与自己异或结果为 0)。
  • 恒等性:x ^ 0 = x(任何数与 0 异或结果都为自己)。
  • 交换性:x ^ y = y ^ x(异或运算的顺序不影响结果)。

~0 的打印结果为 -1,是因为 0 按位取反为 1111 1111,而负数在内存中是按照补码的形式存储的,也就是说 1111 1111 是一个补码,那么它的反码就是 1111 1110,原码就是 1000 0001,也就是 -1。

x |= (1<<(n-1)):可以指定 x 的某一比特位置为 1。

x &= (~(1<<(n-1))):可以指定 x 的某一比特位置为 0。


4、单目操作符

(1)sizeof

sizeof 以字节为单位,它不是函数,是一个关键字。

sizeof 操作符正确的用法:sizeof(type) 或 sizeof(variable),其中 type 必须用括号括起来。

sizeof('') 会发生报错;sizeof("") 结果为 1;char c=0, sizeof('c') 根据 C99 标准的规定,'c' 叫作整型字符常量,为 int 型,故结果是 4(32 位机器);而 ISO C++ 规定,'c' 叫作字符字面量,为 char 型,故结果是 1。char a=0; sizeof(~a)、sizeof(a<<1)、sizeof(a>>1) 的结果都为 4,因为 char 类型数据只有 8 比特位,读到寄存器中,只能填补低 8 位,那么高 24 位就需要进行 “整型提升”。


(2)!

打印 !0 结果为 1


5、逻辑运算符

(1)A && B

如果 A 为假 (0),则 B 不会被计算。因为如果第一个操作数为假,整个表达式的结果已经确定为假,此时右边的表达式被称为 “短路”。


(2)A || B

如果 A 为真 (非 0),则 B 不会被计算。因为如果第一个操作数为真,整个表达式的结果已经确定为真,此时右边的表达式被称为 “短路”。


(3)区分逻辑与和按位与,区分逻辑或和按位或

  • 1 & 2 ——> 0
  • 1 && 2 ——> 1
  • 1 | 2 ——> 3
  • 1 || 2 ——> 1

6、接续符

续行功能,但不能在 \ 之后带上空格。


九、表达式求值

1、隐式类型转换

(1)整型提升

表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

通用 CPU 是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以表达式中各种长度可能小于 int 长度的整型值,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度( int 或 unsigned int),然后才能送入 CPU 去执行运算。

整型提升是按照变量的数据类型的符号位来提升的。

char c=1; 那么 sizeof(c)=1,sizeof(!c)=1,sizeof(+c)=4,sizeof(-c)=4。c 只要参与表达式运算,就会发生整型提升。


2、操作符的属性

(1)问题表达式

a*b + c*d + e*f,表达式的计算机顺序就可能是:

  • a * b,c*d,a*b + c*d,e*f,a*b + c*d + e*f
  • a*b,c*d,e*f,a*b + c*d,a*b + c*d + e*f
  • 由于 * 比 + 的优先级高,只能保证第一个 * 的计算是比第一个 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。

c + --c 操作符的优先级只能决定 -- 的运算在 + 的运算的前面,但是没办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,存在歧义。

i = i-- - --i * ( i = -3 ) * i++ + ++i 在不同编译器中测试结果不一样

answer = fun() - fun() * fun(); 虽然在大多数的编译器上求得结果都是相同的,但是这里只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。

ret = (++i) + (++i) + (++i) 第一个 + 在执行的时候,第三个 ++ 是否执行是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。


十、关键字

1、static

(1)修饰局部变量

static 修饰局部变量(代码块中的变量)改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。


(2)修饰全局变量

在编译的时候会出现连接性错误。一个全局变量被 static 修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。

static 修饰全局变量影响的是作用域的概念,函数类似,而生命周期是不变的。


(3)修饰函数

在编译的时候会出现连接性错误。一个函数被 static 修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。


2、register

声明寄存器变量。

尽量将所修饰的变量放入 CPU 寄存器中,从而达到提高效率的目的。

几种情况下的变量可以采用 register

  • 局部的(全局会导致 CPU 寄存器被长时间占用)
  • 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register 就失去了它的意义)
  • 高频被读取的(提高效率所在)

不要大量使用,因为寄存器的数量有限。

register 修饰的变量不能取地址(因为已经放在寄存器中了)


3、switch - case

  • case 语句后面不可以是 const 修饰的只读变量。
  • default 可以出现在 switch 内的任何部分,但强烈推荐放在 case 语句的最后。

4、void

void 不能定义变量。

void 作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待。所以,既然无法开辟空间,那么也就无法作为正常变量来使用,那么编译器干脆就不让它定义变量,导致 sizeof(void) 在不同编译器下得到的结果不同。

如果一个函数没有参数,将参数列表设置成 void,例如:int test(void) {},因为假如依旧传入参数:test(10);,编译器会告警(vs)或者报错(gcc),那么错误可以明确提前被发现。

void* 的作用是用来接受任意指针类型的。

void * 定义的指针变量不可以进行运算操作(void* p=NULL; p++; p+=1;)。

void 类型是不能直接解引用访问所指内存空间的,需要强制转换成其他具体类型才能使用。


5、const

(1)const 修饰指针变量

  1. const 如果放在 * 的左边(const int* p = &n;),修饰的是指针指向的内容,保证指针指向的内容(*p = 20;)不能通过指针来改变,但是指针变量本身的内容(p = &m;)可变。
  2. const 如果放在 * 的右边(int* const p = &n;),修饰的是指针变量本身,保证了指针变量的内容(p = &m;)不能修改,但是指针指向的内容(*p = 20;)可以通过指针改变。

const 修饰的变量并非是不可被修改的常量,const 修饰变量的意义:

  1. 让编译器进行直接修改式检查
  2. 告诉其他程序员这个变量后面不要进行修改,也属于一种 “自描述” 含义

const 修饰的变量不可以作为数组定义的一部分(const int n=10; int arr[n];)。

const 是在编译期间起效果。


6、volatile

读取的时候,每次都要从内存读。忽略编译器的优化,保持内存的可见性。


7、typedef

用以给数据类型取别名,但不能当成简单的宏替换。


十一、指针

指针是一个变量,存放内存单元的地址(编号),地址是唯一标示一块地址空间的。(存放在指针中的值都被当成地址处理)

指针就是地址,地址的本质是数据,数据可以被保存在变量空间里面。

  • 严格意义上来说,指针和指针变量是不同的,指针就是地址值,而指针变量是 C 语言中的变量,要在特定区域开辟空间,用来保存地址数据,还可以被取地址。

1、内存

  • 为了有效的使用内存,把内存划分成一个个小的内存单元,每个内存单元的大小是 1 个字节。

2、指针变量的大小

  • 指针大小在 32 位平台是 4 个字节,64 位平台是 8 个字节。

3、指针类型

  • 指针的类型决定了指针向前或者向后走一步有多大(距离)。
  • 指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问 1 个字节,而 int* 的指针的解引用能访问 4 个字节。

4、野指针

  • 局部变量指针未初始化,默认为随机值
  • 当指针指向的范围超出数组的范围时,就是野指针
  • 指针指向空间未及时释放

5、数组

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

指针和数组指向或者表示一块空间时,访问的方式是可以互通的,具有相似性。但是具有相似性不代表是同一个东西或者具有相关性,指针和数组二者没有关系。

数组名表示的是数组首元素的地址。


(1)指针数组

  • 指针数组是存放指针的数组。
  • int *p[10]:p 先和 [ ] 结合,说明 p 是一个数组,一个包含 10 个 int* 的数组,即每个元素都是指向 int 类型的指针。
  • int (*p[10])[5]:p 是一个数组,包含 10 个元素,每个元素是一个指向 int[5] 类型的指针,表示它指向一个包含 5 个 int 元素的数组。

6、数组指针

数组指针是指针,指向数组的指针。

int (*p)[10]:p 先和 * 结合,说明 p 是一个指针变量,然后指向的是一个大小为 10 个整型的数组。所以 p 是一个指针,指向一个数组,叫作数组指针。
注意:[ ] 的优先级要高于 *,所以必须加上 ( ) 来保证 p 先和 * 结合。

&数组名和数组名

  • int arr[10]; &arr 和 arr 的值虽然是一样的,但是其意义是不一样的。arr 是数组名,数组名表示数组首元素的地址。&arr 表示的是数组的地址,而不是数组首元素的地址。数组的地址 +1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40。

数组指针指向的是数组,那么数组指针中存放的是数组的地址。


7、二级指针

  • 指针变量也是变量,是变量就有地址,存放指针变量的地址的变量就是二级指针 。

8、指针参数

(1)一级指针传参

当一个函数的参数部分为一级指针时,以整型元素为例,函数能接收的参数只要传的是一个整型元素的地址就行(数组首元素地址,一个变量的地址,一个一级指针)。


(2)二级指针传参

当函数的参数为二级指针的时候,可以接收的参数包括二级指针、一级指针的地址以及数组的地址。


在 C 语言中,只要函数调用,必定发生拷贝。


9、函数指针

void (*fun1)(); 和 void *fun2(); 中哪个有能力存放 test 函数的地址?首先,能给存储地址,就要求必须是指针,所以 fun1 可以存放。fun1 先和 * 结合,说明 fun1 是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为 void。

(*(void (*)())0)():将 0 强制类型转换为类型为 void (*)() 的函数指针,然后去调用 0 地址处的函数。

void (*f(int , void(*)(int)))(int):f 函数先与 () 结合,f 函数的参数有 2 个,第一个是 int 类型,第二个是函数指针类型,该函数指针能够指向的那个函数的参数是 int,返回值类型是 void。f 函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是 int,返回类型是 void。

  • 可简化:typedef void(*pfun_t)(int); pfun_t f(int, pfun_t); 将函数指针 void(*)(int) 的类型重定义为 pfun_t,这样在声明函数的时候就一目了然了。

10、指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组 ,数组的元素都是函数指针。

  • 函数指针 pfun:void (*pfun)(const char*) = test;
  • 函数指针的数组 pfunArr:void (*pfunArr[5])(const char* str); pfunArr[0] = test;
  • 指向函数指针数组 pfunArr 的指针 ppfunArr:void (*(*ppfunArr)[10])(const char*) = &pfunArr;

十二、回调函数

回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是一个回调函数。

模拟实现 qsort(采用冒泡的方式)


十三、结构体

1、特殊的声明

警告: 编译器会把上面的两个声明当成两个不同的类型,所以是非法的。


2、结构体内存对齐

(1)结构体的对齐规则

  1. 第一个成员在与结构体变量偏移量为 0 的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处(对齐数 = 编译器默认的一个对齐数(VS 中默认的值为 8)与该成员大小的较小值)
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体,那么嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

为什么存在内存对齐?
  1. 1、平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 2、性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因是为了访问未对齐的内存,处理器需要进行两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说,结构体的内存对齐是拿空间来换取时间的做法。做法:应该让占用空间小的成员尽量集中在一起。


3、修改默认对齐数

结构在对齐方式不合适时,可以自己更改默认对齐数,通常情况下设置为 2 的 n 次方。


4、结构体传参

结构体传参的时候,要传结构体的地址。函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象时,结构体过大,参数压栈的的系统开销比较大,会导致性能的下降。


5、位段

(1)位段的声明和结构是类似的,但它们也有不同之处

  1. 位段的成员必须是 int、unsigned int 或 signed int 或者是 char (属于整型家族)类型
  2. 位段的成员名后边有一个冒号和一个数字

跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在


(2)位段的内存分配

位段的空间是按照需要以 4 个字节(int)或者 1 个字节(char)的方式来开辟的

位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段


(3)位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定(16 位机器最大是 16,32 位机器最大是 32,写成 27,在 16 位机器会出问题)
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

(4)应用

IP 分装包中的一种格式就有用到位段


十四、枚举

默认从 0 开始,一次递增 1,也可以在定义的时候赋初值。

相较于使用 #define 定义,选择常量枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨
  3. 防止命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

十五、联合(共用体)

联合的成员是共用同一块内存空间的,这样一个联合变量的大小至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍

在同一时间内只可以使用联合体中的一个成员

当使用成员 i 时,对于 c 来说,此时 c 的那一个字节里面存的是 i 的内容,就相当于此时 c 是不存在的。因为它们共用一块空间,先给 un.i 赋值,再给 un.c 赋值,就会改变 1 个字节的内容(因为 un.c 是 1 个字节)。


十六、动态内存管理

1、动态内存开辟是在堆上开辟的


2、动态内存函数

(1)malloc

向内存申请一块连续可用的空间,并返回指向这块空间的指针

cplusplus.com/reference/cstdlib/malloc/?kw=malloc

如果开辟成功,则返回一个指向开辟好空间的指针。如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查

返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定

如果参数 size 为 0,malloc 的行为是标准还是未定义的,这取决于编译器

声明都在头文件 stdlib.h 中


(2)free

用来做动态内存的释放和回收的

cplusplus.com/reference/cstdlib/free/

如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的

如果参数 ptr 是 NULL 指针,则函数什么事都不做,否则使用完并 free 之后一定要记得将其置为空指针

声明都在头文件 stdlib.h 中


(3)calloc

用来动态内存分配

cplusplus.com/reference/cstdlib/calloc/

功能:为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0

声明都在头文件 stdlib.h 中


(4)realloc

让动态内存管理更加灵活

cplusplus.com/reference/cstdlib/realloc/?kw=realloc

  • ptr 是要调整的内存地址,size 为调整之后的新大小。
  • 返回值为调整之后的内存起始位置。

在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

realloc 在调整内存空间时存在两种情况:

  1. 原有空间之后有足够大的空间(要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化)
  2. 原有空间之后没有足够大的空间(扩展方法:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址)

如果 realloc 找不到合适的空间,就会返回空指针。如果想让它增容,它却存在返回空指针的危险,所以不要拿指针直接接收 realloc,可以使用临时指针先判断一下

当要调整的内存地址为 NULL 时,realloc 的功能相当于 malloc

声明都在头文件 stdlib.h 中


3、常见的动态内存错误

对 NULL 指针的解引用操作

对动态开辟空间的越界访问

对非动态开辟内存使用 free 释放

使用 free 释放一块动态开辟内存的一部分:

对同一块动态内存多次释放

动态开辟内存忘记释放,会造成内存泄漏


4、C / C++ 中程序内存区域划分

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收 。(分配方式类似于链表)
  • 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
  • 实际上普通的局部变量是在栈区分配空间的,栈区的特点是:在上面创建的变量出了作用域就销毁,但是被 static 修饰的变量存放在数据段(静态区)。数据段的特点:在上面创建的变量直到程序结束才销毁,所以生命周期变长 。

5、柔性数组(flexible array)

结构中的最后一个元素允许是未知大小的数组,这就叫作柔性数组成员


(1)特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

(2)优势

  1. 方便内存释放(如果是在一个给别人用的函数中,在里面做了二次内存分配,并把整个结构体返回给用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,不能指望用户来发现这个事。所以,如果把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉)
  2. 有利于提高访问速度(连续的内存有益于提高访问速度,也有益于减少内存碎片(其实也没多高,反正都要用做偏移量的加法来寻址))

也可以使用一个在结构体中引入一个指针成员,达到相似的效果:


十七、文件操作

1、程序文件

包括源程序文件(后缀为 .c)、目标文件(windows 环境后缀为 .obj)、可执行程序(windows 环境后缀为 .exe)。


2、数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件

文件名包含 3 部分:文件路径+文件名主干+文件后缀

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

字符一律以 ASCII 形式存储,数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储

  • 如有整数 10000,如果以 ASCII 码的形式输出到磁盘,则磁盘中占用 5 个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占 4 个字节(VS2019)


文件在读写之前应该先打开文件(fopen),在使用结束之后应该关闭文件(fclose)


3、scanf / fscanf / sscanf

(1)scanf

标准输入流。

当 scanf() 检测到 “文件结尾” 时,会返回 EOF,通常用 #define 指令把 EOF 定义为 -1


(2)fscanf

用于对格式化的数据进行读取,从流 stream 读取格式化输入,适用于所有输入流(stdin 和 stdout 的本质也是文件流)


(3)sscanf

从一个字符串中读入格式化的数据,默认读入是从 stdin 中读入


4、printf / fprintf / sprintf

(1)printf

标准输出流,将一个格式化的字符串输出到屏幕

%-10d:说明打印的数字位于字段的左侧,对应的输出结果有 10 个空格宽度。


(2)fprintf

将一个格式化的字符串写入文件中


(3)sprintf

将一个格式化的字符串输出到一个目的字符串中


5、文件的随机读写

(1)fseek 文件指针定位函数

根据文件指针的位置和偏移量来定位文件指针

origin:

  • SEEK_CUR - 当前文件指针的位置开始偏移
  • SEEK_END - 文件的末尾位置开始偏移
  • SEEK_SET - 文件的起始位置开始偏移

(2)ftell 返回偏移量函数

返回文件指针相对于起始位置的偏移量


(3)rewind 文件指针回到起始位置函数

设置文件位置为给定流 stream 的文件的开头,让文件指针回到起始位置


6、文件结束判定

(1)feof

在文件读取过程中,不能用 feof 函数的返回值直接来判断文件是否结束,而是应用于当文件读取结束时,判断是读取失败结束,还是遇到文件尾结束

文本文件读取是否结束,判断返回值是否为 EOF (fgetc),或者 NULL(fgets)

fread 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。fread 函数在读取结束时会返回实际读取到的完整元素的个数,如果发现读取到的完整的元素个数小于指定的元素个数,那么就是最后一次读取了

cplusplus.com/reference/cstdio/fread/


十八、Debug & Release

  • Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
  • Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用

十九、预处理

1、预定义符号(这些预定义符号都是语言内置的)

  • __FILE__:进行编译的源文件
  • __LINE__:文件当前的行号
  • __DATE__:文件被编译的日期
  • __TIME__:文件被编译的时间
  • __STDC__:如果编译器遵循 ANSI C,其值为 1,否则未定义,用例判断该文件是不是标准 C 程序

2、#define

(1)#define 定义标识符

如果定义的内容过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符,后面不能加空格)

在 define 定义标识符的时候,不要在最后加上 ;


(2)#define 替换规则

宏不能出现递归

当预处理器搜索 #define 定义的符号时,字符串常量的内容并不被搜索

宏定义代表字符串的时候,一定要带上双引号

程序翻译过程

  • 预处理-E :头文件展开,去注释,宏替换
  • 编译-S : 将语言编译成为汇编语言
  • 汇编-c :将汇编翻译成为目标二进制文件
  • 链接:将目标二进制文件与相关库链接,形成可执行程序

如果用宏定义充当注释符号,那么在预处理期间:先执行去注释,再进行宏替换(C 编译特征)

宏定义是不能有空格的,使用的时候可以带空格,但是不推荐。


(3)assert 就是一个判断条件是否成立的宏

#define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )


(4)# 和 define 中间是可以有空格的,但是不推荐。


(5)#和##

A. 只有当字符串作为宏参数的时候才可以把字符串放在字符串中


B. 使用 #,把一个宏参数变成对应的字符串


C. ##的作用

可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符


(6)带副作用的宏参数

表达式求值的时候出现的永久性效果:


(7)宏和函数对比

A. 优势
  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于来比较的类型
  • 宏是类型无关的。宏的参数可以出现类型,但是函数做不到

B. 劣势
  • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  • 宏是没法调试的
  • 宏由于类型无关,也就不够严谨
  • 宏可能会带来运算符优先级的问题,导致程容易出错


3、#undef

这条指令用于移除一个宏定义,可以用来限定宏的有效范围。

宏的有效范围,是从定义处往下有效


4、命令行编译

假定某个程序中声明了一个某个长度的数组,如果机器内存有限,需要一个很小的数组,但是另外一个机器的内存比较大,就需要那个数组能够大一些

  • gcc -D ARRAY_SIZE=10 test.c

5、条件编译

在编译一个程序的时候,如果要将一条语句(一组语句)编译或者放弃是很方便的。比如,调试性的代码删除又觉得可惜,保留又觉得碍事,所以可以选择性的编译


(1)常见的条件编译指令

(2)条件编译的本质是让编译器进行代码裁剪


(3)好处

  1. 可以只保留当前最需要的代码逻辑,其他去掉,减少生成的代码大小
  2. 可以写出跨平台的代码,让一个具体的业务在不同平台编译时,可以有同样的表现

(4)#if 和 #ifdef 的区别

  • #if 指令后面跟着一个表达式,如果该表达式的值为非零,则编译后面的代码;如果值为零,则忽略后面的代码
  • #ifdef 指令后面跟着一个标识符,如果该标识符已经被定义过,则编译后面的代码;如果该标识符没有被定义过,则忽略后面的代码。#ifdef 必须要有 #endif 配合使用

(5)宏替换先于条件编译执行


6、文件包含

条件编译可以避免头文件重复引入

每个头文件的开头写

或者

#include 本质是把头文件中的相关内容直接拷贝至源文件中


7、其他预处理指令

#error:输出一个错误信息,核心作用是可以进行自定义编译报错

#pragma:为编译程序提供非常规的控制流信息,可跟 once,message 等许多参数

  • #pragma message():可以用来进行对代码中特定的符号(比如其他宏定义)进行是否存在进行编译时的消息提醒

#pragma warning(disable:4996):禁止 4996 报错

#line:改变当前的代码行数和文件名称

#pragma pack()


二十、函数栈帧

1、相关寄存器

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

2、相关汇编命令

mov:数据转移指令

push:数据入栈,同时 esp 栈顶寄存器也要发生改变

pop:数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变

sub:减法命令

add:加法命令

call:函数调用

  1. 压入返回地址
  2. 转入目标函数

jump:通过修改 eip,转入目标函数,进行调用

ret:恢复返回地址,压入 eip,类似 pop eip 命令


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

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

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

在对函数的形参存储时,编译器是从函数的形参的右边到左边逐一地压栈,这样保证了栈顶是函数的形参的第一个参数(从左到右数)


二十一、命令行参数

main 函数也是一个函数,也可以携带参数。int main( int argc, char *argv[ ], char *envp[ ] ) {}

  • 第 1 个参数: argc 是个整型变量,表示命令行参数的个数(含第一个参数)
  • 第 2 个参数: argv 是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字符串)。argv 数组的最后一个元素存放了一个 NULL 的指针
  • 第 3 个参数: envp 是字符指针的数组,数组的每一个元素是一个指向一个环境变量(字符串)的字符指针。envp 数组的最后一个元素也存放 NULL 指针

这篇关于【C语言】零碎知识点(易忘 / 易错)总结回顾的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1137019

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

基本知识点

1、c++的输入加上ios::sync_with_stdio(false);  等价于 c的输入,读取速度会加快(但是在字符串的题里面和容易出现问题) 2、lower_bound()和upper_bound() iterator lower_bound( const key_type &key ): 返回一个迭代器,指向键值>= key的第一个元素。 iterator upper_bou

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚: