重读经典:C和指针学习笔记
重读经典:《C和指针》学习笔记
《C语言与指针》、《C语言专家编程技巧》、《C语言陷阱与常见错误》被誉为学习C语言的三大经典著作。多年以前笔者曾囫囵吞枣地研读过这三部经典的书籍,并将其收藏起来长期未加利用。直至今日几乎遗忘了大部分内容,在偶然翻开这些经典著作后又开始重新研读,并记下了一些个人体会。
《C语言与指针》通过深入探讨了关于指针的基础知识及其高级特性等内容,旨在帮助程序员更好地运用这一技术于实际编程工作中。
第1章 快速上手
本章阐述了C语言的基础知识内容,并旨在帮助读者形成一个全面的基本认知。
- 在使用〖/〗创建代码块的时候,请特别留意其中是否存在〖/〗结束符,并且还有一种〖〖/〗〗式的块注影方法。
- 特别提醒,在编程过程中需要区分开=与==以及&与&&之间的区别。
- 当调用 scanf 函数的时候,请确保接受的数据类型的参数是带有指针类型的变量,并记得附加 & 运算符。
- 通过包含指令(include)可以在一定程度上减少代码中的冗余部分。
- getchar() 函数返回的是一个整数值而非字符型数据(参考《C陷阱与缺陷》一书)。
第2章 基本概念
第二章仍然讲C语言的基础知识,主要知识点包括:
1、程序的编译链接环境和运行环境可以有所不同;
程序的编译与链接过程主要包括两个阶段:首先是编译阶段(compilation),该阶段将源代码转换为可执行代码,并处理预处理指令如#define和#include;随后进行链接操作(linking),以整合外部库资源并生成最终可执行文件。
2、标识符
对于标识符的长度而言,在ANSI标准中明确规定了外部标识符应通过其前6个字符实现唯一区分功能,并且在标称上并不考虑大小写的差异;而对于内部标识符,则要求其在前31个字符的基础上才能确保唯一识别的需求得以满足。(《C陷阱和缺陷》一书中也曾对此有过详细阐述)
为了防止与C语言保留关键字发生混淆或冲突,在定义标识符时必须格外谨慎。
3、字符转义
在C语言中使用特定符号如'\'来表示一些不可直接显示的ASCII码。例如零结束符(\0)、换行符(\n)和制表符(\t)等都属于此类情况。这些后续的字符串不再保留原始ASCII编码的意义。
而大多数程序开发者仅熟悉第一种方法。
在C语言中存在两种不同的转义方式:
一种是通过在单个字符前加上反斜杠符号(\),另一种则使用三个字母构成的控制序列。
三字母词就是几个字符的序列,合起来表示另一个字符,比如
| 三字母词 | 含义 |
|---|---|
| ??( | [ |
| ??< | { |
| ??= | # |
| ??) | ] |
| ??> | } |
| ??/ | | |
| ??! | |
| ??’ | ^ |
| ??- | ~ |
“\”转义
部分转义字符定义:
| 转义字符 | 含义 |
|---|---|
| \a | 警告字符,可能会奏响铃声或者产生一些其它可见字符 |
| \b | 退格 |
| \f | 进纸 |
| \n | 换行 |
| \r | 回车 |
| \t | 水平制表符 |
| \v | 垂直制表符 |
| \ddd | 表示1-3个八进制数字,给定的八进制数转义为对应的ASCII字符 |
| \xddd | 表示1-3个十六进制数字,给定的十六进制数转义为对应的ASCII字符,这个值大小可能超出范围 |
1.\vert和\f控制屏幕显示的垂直制表与页面切换
2.正确称呼为Enter键执行的行转换
3.光标向右跳转四个或八个字符位置
4.按住Backspace键可删除前一个字符
5.这些功能源自于早期电子打字机的技术演变
4、编程风格
C语言以其非正式的形式存在,并具有较为宽松的语法规则。然而,在遵循良好的程序风格并提供详尽的文档说明后,则可以使代码变得更加易读且易于维护。
第3章 数据
第3章仍然讲C语言的基础知识,主要讲数据类型的定义。
1、C语言数据类型
C语言的数据型别可分为基储汇编与指针型别;基储型别主要有整數型別int,字符型別char,浮點數型別float与double;而汇编与指针型别则主要有結構體struct,數組array, Enums enum與联合體union;具體有哪些請參見下圖:

需要注意的地方包括:
ANSI标准规定了相关细节。
其中特别指出:
长号数值类型应与基本数值类型在尺寸上相仿
小号数值类型同样不小于16 bit
然而关于数值大小并未有统一的规定
具体实现时可能由不同编译器设定
例如,在某些情况下所有这些数值类型可能都被指定为32 bit宽
为了表示字符数据而设计
其实质是一个8-bit的数据结构
2、指针类型
int a;
inta;
这两种说明的效果完全相同,都是将变量a赋值为一个指向整数类型的指针。
但是,在需要定义多个指针变量时正确的做法是:使用int *a,*b,c;而不是采用int a,b,c的方式。
3、常量
变量使用const关键字进行声明,在运行期间其值无法更改。
一种是,在定义变量时直接指定其初始值;
另一种是在函数被调用时,
参数会被赋值与其对应的实参。
对于int * const类型的变量p而言,在此定义下p为不可变的常量指针;它指向内存中的固定位置。
在int const * a这样的结构中p为可变的非恒定指针;它指向内存中的固定位置。
它们的本质区别在于*p是否为可变或不可变的引用方式。
4、作用域
标识符的作用域类型共有四种。
【
- 文件作用域是基于文件系统层次的。
- 代码块范围内的变量是通过引用的方式被识别的。
- 函数声明中的返回地址标识符用于传递程序控制流。
- 原型作用域下,则是以形参名称作为识别依据。
5、static关键字
static关键字有两种主要用途:
首先,在修饰全局变量和函数时,它会影响它们的链接属性,并使外部不可见。
其次,在修饰局部变量时,则会决定它们的数据存储方式。
最后,在C++编程中,默认情况下(static未被声明),通过使用#pragma once指令可以在编译阶段限制std::vector等容器的功能特性以实现特定需求。
6、隐式类型转换
遵循以下算术计算规则,在不同类型的数值数据进行加减乘除取余以及符号运算时(即进行数值型数据的四则运算及取余操作),必须确保所有参与计算的数据均为同一数据类型的数值数据方能完成相应的计算操作。遵循以下原则:
在整型提升机制下:所有小于int类型的变量会被系统自动转换为int类型。这些包含于该机制的数据类型有以下几种:char、signed char、unsigned char、short以及unsigned short。
当执行运算时,在处理过程中主要依据表达式中最长的数据类型来统一处理所有其他数据类型的数值
若运算数中包含double型或float型数据,则其余的数据全部转换为double类型后参与运算
(2)若运算数中最长的类型为long型.则其他类型数均转换成long型数。
当参与运算的所有操作数中最大数据类型的数值为整数类型时(即int),则字符类型的数据也会被自动转换为整数类型的数据参与运算。该算术操作在执行过程中会自然地被处理。
若操作数的最大数据类型的长度包括signed和unsigned int时,则signed会被转换成unsigned。
当操作数中的最长数据类型包括带有符号的long整型和无符号的long整型时,该符号型的数据会被转换为无符号型.
(6)若运算数中最长类型包含float 与double,float会转换为double。
如果运算数中存在最大数据类型的数值既包含double又包含long double,则双精度型会被转换为long double类型。
在C语言中,默认使用整型精度执行算术运算。这些数据类型的变量,在使用时会被自动提升为整型。例如 char a, b, c; a = b + c; 在这一过程中,b和c会被转换为整型并完成加法运算后结果截断赋值给c
在执行赋值操作时,在使用赋值运算符右侧的数据类型时,请确保其类型与赋值号左侧一致;如果右侧数据类型的长度超过左侧,则需对数据进行截断处理或取整处理。
特别地,在执行自动数据类型转换的过程中
unsigned char c=0xff;
int a;
unsigned int b;
a=c;
b=c;
运行结果:a=0xff,b=0xff
char c=0xff;
int a;
unsigned int b;
a=c;
b=c;
运行结果:a=0xffffffff,b=0xffffffff
代码解读
第4章 语句
第4章仍然讲C语言的基础知识,主要讲语句。
1、printf返回值
实际上, printf 函数具有返回值特征。按照 MSDN 的描述, 返回值通常表示打印出的有效字符数量, 如果出现错误则会返回负数值。
其实sprintf函数也有返回值,返回值是写入buffer中的字节数。
2、for语句
for(a,b,c) // a为初始化操作,在进入循环体之前会被调用一次。
statement; // 声明一条说明信息。
a: 将在每次进入循环体之前被调用一次。
b: 将在每次进入循环体之前进行判断。
c: 将在每次退出当前循环体之后,并重新进入b阶段之前的逻辑进行状态更新。
该for语句允许使用break和continue关键字来控制流程。
3、switch语句
每个case标签应当包含一个唯一且固定的常量值或常量表达式,在编译阶段即可对其进行计算。
switch语句中包含有continue语句这一语法特性,并且其不具备任何实际作用。
特别提醒:在case标签结束后一定不要忘记添加对应的break关键字以保证程序逻辑完整性。
第5章 操作符和表达式
1、移位
ANSI标准规定无符号数的移位属于逻辑移位运算,在编译阶段有符号数的移位性质取决于特定编程语言或编译器的规定情况。
2、表达式
按照从右到左的结合顺序, 表达式x=y=a+3会被解析为两条独立指令, 首先执行y=a+3, 然后将结果赋给变量x. 这样的结构在语法上是合法且可执行的. 然而ANSI标准并未规定赋值运算符的确切优先级, 编译器在解析时可能会先评估左边操作数, 也有可能会优先处理右边的操作数. 因此, 在某些情况下像*p++=*p++;这样的代码可能会导致不同实现间的不兼容性. 逗号不仅能够将多个简单指令合并到一个复合结构中, 还能有效提升代码的整体可读性和维护性.
while(x<y)
x=2,y=3;
代码解读
这样写是合法的,但这并不是好的写法。
数组的下标引用和间接访问表达式是等价的,a[下标]=*(a+下标);
3、布尔变量
由于C语言缺乏明确定义的布尔类型,在实际应用中通常会使用整数来替代时会采用以下方式设定数值范围。其中将数值设为0时对应于假值;而所有非零数值均被视为真值。其中"1"这个具体取值并不会因为其特殊性而区别对待其他非零取值
4、左值和右值
左操作数与右操作数
左边操作数可做为赋植运算符之左侧之对象;
右边操作数可做为赋植运算符之右侧之对象;
左边操作数可转换为右边操作数形态;但右边操作数并不一定能转换为主动左边操作数形态。
变量可做为主动左边操作数形态;同时包含下标引用及间接访问表达式也可作为主动左边操作数形态。
第6章 指针
指针是C语言的精华之所在,具有强大的灵活性,也是最容易出问题的地方。对于初学者而言,指针难以理解和掌握。即使对于有经验的程序猿,用好指针也不是一件容易的事。对指针的掌握程度已经程序检验一个程序猿的试金石。
指针的本质是一个变量,这个变量中保存的值被解释为内存地址。这个内存地址可以指向不同的数据类型,比如指向char类型,int类型或者float类型甚至指向指针类型。
对于固定架构的cpu来说,指针变量的长度是固定的,在x86计算机上,指针变量的长度是4。这也是指针强大的灵活性的基础,也是指针强制类型转换的基础。
对于cpu而言,在内存地址中存储的数据都是0、1这样的二进制位,至于长度是1个字节、2个字节或者4个字节,完全由程序开发者决定。比如:
int *a=100;
char *b=(char*)a;//a和c指向同一个内存地址,在解引用时*a访问的是4个字节,*c访问的是一个字节
int c;
char d;
c=*a; //对应汇编语言 mov c,DWORD PTR [a]
d=*b; //对应汇编语言 movsb b,DWORD PTR [b]
代码解读
操作可以使指针执行算术运算。
当向指针中添加一个整数值时,在内存地址上会使该指针的位置发生偏移。
例如:创建一个指向变量名的指针实例,并将其赋值给另一个变量。
int *a,*b;
b=a;
a+=1;//此时b-a=sizeof(int)
代码解读
两个指向相同数据类型的指针可进行减法运算。程序猿应确保这两个指针指向同一数组或由malloc申请的内存区域。解引用空指针在大多数操作系统中会导致程序崩溃;如果程序未崩溃,则可能引发潜在漏洞。声明一个指针不会自动分配内存资源。再次提醒:p++与(p++)功能相同。
第7章 函数
1、函数定义和函数声明
函数定义等同于函数体,并位于代码块内呈现。该部分主要负责将具体的实现细节有条理地组织起来。
需要注意的是,在某些情况下该部分也可能完全不返回值而无需传递给调用者。
此外,在编程过程中我们通常会将该功能模块单独提取出来形成一个独立的功能模块。
另外一种情况是该功能模块可能完全不返回值而无需传递给调用者。
为了保证代码的安全性和可维护性,
我们需要对所有类似的模块进行统一命名和管理,
以确保不同位置出现的相同名称不会产生冲突。
2、函数参数
函数调用时参数传递有两种方式,
在传值调用机制中,在函数被调用时会生成一份参数副本。对于这些操作而言,在这种情况下形参不会影响到实际传递给被调用函数的参数。通过反汇编分析程序的机器码指令序列,在程序运行期间可观察到参数是如何被传递到寄存器中的。具体来说,在这种情况下数据会被压送到栈顶位置。这样使得实际传递给目标函数的是独立于源程序中的参数
我们常说传址调用是指用于表示该对象在内存中的位置的一种操作方式。具体来说,输入的数据是用于表示该对象在内存中的位置信息,通过获取该位置信息来解引用,从而实现对实参值的具体更改。当传递的数据名称时,程序会根据具体情况采取不同的处理方式:理论上讲,在C语言中,一个合法的数据名称应该代表整个数据序列的位置起始点,然而实际上,由于数据类型的限制以及编译器实现的原因,通常情况下数据名称仅能表示其首元素的位置信息与指针等价。因此在实际应用中,无论是否使用合法的数据名称来进行传递操作时的行为都与直接使用指针的方式一致:即不会执行任何额外的数据拷贝操作,而是退化为简单的寻址操作形式。
3、递归
递归函数是指在其定义中直接或间接调用自身的函数。
一旦掌握递归函数的概念后,最简便的方式是不深入研究其运行机制,而是信任该函数能够完成预期的任务。
教材中通常选取两个典型案例来说明递归函数的应用及其编程实现方式:一个是计算阶乘问题(factorial),另一个是生成斐波那契数列(Fibonacci sequence).以下是用递归方法实现这两个问题的具体代码:
对于阶乘问题,其算法思路是将一个整数n依次从n降到1,并相乘;而对于斐波那契数列,则是从第3项开始,每一项等于前两项之和。
unsigned int factorial(unsigned int n)
{
if (n==0 || n==1)
{
return 1;
}
return n*factorial(n-1);
}
unsigned int fibonacci(unsigned int n)
{
if (n==0)
{
return 0;
}
else if (n==1)
{
return 1;
}
return fibonacci(n-1)+fibonacci(n-2);
}
代码解读
在计算阶乘时,并未展现出使用递归方法的明显优势;而采用递归算法来求解斐波那契数列问题时,则会导致冗余的计算过程和较低的效率。
void myitoa(unsigned int n)
{
unsigned int quotient;
quotient=n/10;
if (quotient)
{
myitoa(quotient);
}
printf("%d",n%10);
}
代码解读
在编程中定义:当一个函数在其体内执行的最后一行代码直接或间接地调用自身时,则称该行为为"tail recursion"( tail 递推)。这种特性使得程序能够以一种高效的方式重复执行相同的操作而无需重复存储状态信息。
所有具备"tail recursion"特征的程序都可以将其转换为非 recursive 版本以提高性能。大多数编译器采用运行时栈来模拟 tail 过程,在这种情况下进行操作往往会导致内存泄漏风险。
当我们处理阶乘和斐波那契数列相关的计算时, 使用循环结构来重新编写这些算法将有助于提升性能。
unsigned int factorial(unsigned int n)
{
unsigned int result=1;
if (n==0 || n==1)
{
return 1;
}
while(n)
{
result*=n--;
}
return result;
}
unsigned int fibonacci(unsigned int n)
{
unsigned int result=0;
unsigned int prev,prevprev,i;
if (n==0)
{
return 0;
}
else if (n==1)
{
return 1;
}
prev=1;
prevprev=0;
i=2;
while(i++<=n)
{
result=prev+prevprev;
prevprev=prev;
prev=result;
}
return result;
}
代码解读
第8章 数组
1、数组名的本质
在C语言编程中,默认情况下数组名并没有直接的意义;从逻辑上说,人们可能会认为数组名应该代表整个数组,但实际上它只是一个指向该数组第一个元素内存位置的指针变量,即是指向第一个元素所在内存位置的指针.
2、数组与指针的区别
在表达式中使用数组名时,编译器为其生成指向内存单元的指针常量。
当使用typeof运算符获取大小时,在涉及数组名的操作中不会生成指向内存的指针。
声明变量类型为int并初始化五个元素后,在运行期间会在堆栈上分配存储空间;定义指向变量的指针时不进行内存分配。
a是合法有效的,并可访问变量a的第一元素;
通过b引用的内存位置可能无确定值导致程序异常退出。
3、数组引用方式
对于数组而言,在特定场景下具有更高执行效率的间接引用与直接引用本质上相同
4、数组参数
- 数组名作为函数参数时本质上属于传值方式;由于它们被视为静态地址或固定指向对象,在传递过程中会被复制一份副本。
- 通常来说,在函数定义中使用数组和使用带有指向符的变量具有相同的效果。
- 当函数形参未指定尺寸时,默认会触发的是基于地址的传递方式;此时需要额外提供尺寸信息才能满足编译需求。
- 当函数形参附带了尺寸说明时,默认实参必须是一个同样大小的数据结构或对象才能通过验证。
- 不论采用何种定义方式,在计算数据大小方面结果是一致的。
5、数组初始化
- 数组的初始化遵循大括号规则,在括号内各初始值以逗号分隔;
- 当初始化不足时,在缺失的位置,默认赋值为零;
- 初始值数量不得超过元素个数限制,在超出范围时会导致编译错误;
- 若未指定数组长度,则编译器会自动确定一个刚好能容纳所有初始值的最大长度;
- 对于定义在函数体内的数组,在每次函数调用时都会重新进行初始化操作,在声明前添加
static关键字可使其成为静态数据块; - 静态数组和全局变量的数据将在PE文件中的.data段区域分配空间,并且仅会在首次运行时执行一次初始化操作;
- 字符串类型的字符数组同样支持类似的方式进行赋值配置,在这种情况下,“abcdef”的含义是作为赋值列表而非字符串常量使用,并保证最终字符数组的实际长度为七位。
6、多维数组
- 多维数组本质上仍然是基础的一位数组,在访问元素的方式上既可以使用下标方式进行访问。
- 当创建多位数组时,默认情况下只有第一位维度的具体大小是由初始化列表所决定。
- 当将多位数组作为函数或程序中的参数传递时,默认情况下只有第一位维度的具体大小是可以省略不写的。
- 指向一位或多位元素组成的指针的方式有两种:
- 指向一位或多位元素组成的指针的方式有两种:
- int (*p)[10];
- int *p[10];
- 下标运算符在这一层的操作符优先级高于通过调用指针运算符来实现间接操作符的行为。
第9章 字符串、字符和字节
在软件开发中,字符串是一个非常重要的数据类型,并且其处理代码约占整个工程代码量的20%以上。对于新手来说,在进行字符操作时很容易出现溢出问题或者导致溢出漏洞。C语言中没有显式的字符串数据类型(虽然实际上它们存储在字符数组中),换句话说,在这种情况下,默认情况下每个函数都会将一个字符复制到另一个变量里吗?
1、字符串基础
字符以零结束,并非作为有效数据存在于内部。
strlen函数用于获取字符串长度,在此计算中末尾零字符被排除。
需要注意的是:
if(strlen(x)>=strlen(y)) 的逻辑与预期一致;
然而在第二种情况下,
if(strlen(x)-strlen(y)>=0) 由于两次无符号整数相减的结果仍为无符号整数,
其运算结果始终满足条件。
2、长度不受限制的字符串函数
该类字符串操作函数不受限制地计算字符串长度的方法是基于查找字符串结尾处的NUL字符实现的,
这类方法往往会导致溢出的发生。
其中,
\texttt{strcpy}和\texttt{strcat}可能会出现溢出,
它们均返回第一个参数的内容作为副本,
但返回值通常未被使用。
而\texttt{strcmp}则经过了专门的安全性增强处理,
因此不会产生溢出。
3、长度受限的字符串函数
这类函数有strncpy,strncat,strncmp。
4、字符串查找函数
这三个函数均用于查找特定模式并返回相关索引值:
strchr、strii_chr和strstr均用于确定字符串中指定字符首次出现的位置;stripbrk则用于查找任意单个指定字符首次出现的位置;stri spn用于计算从起始位置开始连续匹配指定字符集的总长度;stri cspn则用于反向计算未匹配到第一个不同字符前的有效长度;stri tok作为一个分割函数,在给定分隔符的情况下将输入字符串分解为字段集合,并修改原始字符串内容以反映分割结果。
5、字符函数
将字符串转换成大写, 转换成小写, 判断是否是数字, 检测扩展数字字符, 分辨控制字符, 检查是否存在空格, 确定是否有小写字母, 确认有无大写字母, 测试字母属性, 检查 alphanumeric 类型, 确定标点符号, 分析可打印字符
6、内存操作函数
memcpy:当起始位置与目标位置相同时的结果未定义; memcmp用于比较两个字符串内存内容; memmove其功能类似于memcpy,在某些情况下允许起始位置与目标位置重合; memset则用于初始化内存区域
第10章 结构和联合
C语言支持基础数据类型,并称为标量。
C语言还提供组合数据类型(又称为向量空间),程序猿需自行定义。它们能够同时存储多个基础数据类型(包括数组和结构体)。
1、结构体基础
数组由统一类型的元素构成,在这种情况下可以通过下标的手段来访问这些元素。
数据集合称为成员,在这种情况下它们各自具有不同的属性和长度属性。
需要注意的是,在C/C++语言中,默认情况下不允许一个数据类型包含自身作为成员。
为了实现这种情况我们需要引入一种特殊的指针变量。
特别地,在处理相互依赖的关系时会遇到这样的问题:
即当两个复杂的对象互相引用对方的时候,
这会导致编译器报错或者程序运行时出现栈溢出的情况,
而为了避免这种情况发生,
我们可以选择使用动态内存分配的方式为每个对象分配独立的空间。
struct B;
struct A{
struct B;
int a;
};
struct B{
struct A;
char c;
};
代码解读
2、结构体对齐
对象成员在分配时遵循内存对齐规则。
许多计算机系统规定基本类型数据必须存储在其内存起始地址的一个固定倍数位置上。这意味着这些数据元素必须位于特定数值k的整数倍位置上,并将此数值称为该数据类型的对齐模数(alignment modulus)。这一强制性要求一方面简化了处理器与内存之间的传输机制设计;另一方面有助于提高数据读取效率。
1、内存对齐的原因
大部分参考资料均如此表述:
1、移植原因:多数硬件平台无法直接存取全部内存地址及其所承载的数据;仅有少数平台能在指定地址范围内读取特定类型的数据,并因此产生硬件错误。
2、性能原因:数据结构(特别是栈)应尽量在自然边界上实现自动队列功能;这是因为处理器处理非队列元素需多次同步操作而队列元素则能实现单线程操作。
2、性能原因:数据结构(特别是栈)应尽量在自然边界上进行优化配置;这是因为处理器存取非优化配置区域需经过多次校验而优化配置区域则能提高存取效率
2、对齐的含义
例如这样的一种处理器,在进行每次读写内存操作时总是从特定的八倍地址起始位置开始执行相应的指令序列。该处理器能够依次读取或 writes 8 bytes of data from this starting address, 这样一来,在这种情况下访问单个double类型的变量仅需一次内存操作。然而, 如果软件无法满足上述条件, 在大多数情况下处理这样的变量将需要两次连续的内存操作来完成相应的访问, 因为这些变量可能跨越了相邻且满足对齐条件的两个连续的八字节内存块。
结构体的数据对齐主要包含以下两个方面的内容: 整个结构体所需的总存储空间; 各字段在其所属位置上的相对存储偏移量;或者换句话说, 在整体结构体内每个字段的位置相对于整个结构体起始点的位置。
3、结构体数据对齐规则
数据类型在内存中的布局是指各字段占据的空间边界与相邻字段之间的距离满足特定要求的一种安排方式。对于一个C语言程序来说,在定义一个变量块时,默认情况下会按照一定的规则分配各字段的空间位置:其中第一个字段占据的空间起始位置必定与整个变量块起始位置一致;后续字段的位置则根据其声明顺序以及所占存储空间的数量逐步增加。为了确保整体内存布局符合规定要求,在适当的位置补充不需要的实际内容以达到必要的空间填充(padding)效果。
在字段对齐规则下,在程序定义的数据块内各字段的位置必须满足一定的位置关系要求:
在程序定义的数据块内,
第一个字段的位置必须与整个变量起始位置一致;
每个字段的位置相对于起始位置的位置偏移量必然是该字段占用空间宽度的一个整数值倍;
整个变量占用的空间宽度必须是所有字段占用空间宽度的最大值的一个整数值倍。
3、参数传递
建议在处理结构体变量时采用按值传递的方式。但这种做法会占用大量栈空间资源,请考虑采用按地址传递的方式以节省内存空间。
两个结构体变量之间的关联可通过赋值符号建立。该编译器在处理两个相邻的存储块时会自动完成内存复制。
mov ecx,sizeof(struct)
mov esi,struct_A
mov edi,struct_B
rep movs dword ptr[edi],dword ptr[esi]
代码解读
在C语言中,两个结构体变量之间不能用关系运算符进行比较。
4、联合
所有联合成员在内存中使用了同一位置,并非如表面所示那样单一化;而是根据程序猿的想法被视作不同的数据类型。例如,在某些情况下,4个字节可能被视为int类型或其他用途。
5、位域
数据在存储过程中无需占据完整的一个字节, 而是仅占用一到几个二进制位。例如, 用于表示开关量的状态时, 只有0和1两种状态, 因此只需一位二进制数来表示。目前应用范围有限
第11章 动态内存分配
如果定义一个数组,则该数组的大小必须预先指定,并于运行时期从栈中获取存储空间(例如,在代码中执行sub esp,0x10;);
当一段连续存储空间的实际长度无法预先确定时,则必须采用动态存储管理;
C语言提供了malloc和calloc两个用于动态存储管理的基本函数;
两者的主要区别在于calloc操作完成后会自动将新申请的空间初始化为零;
调用这两个函数后必须判断返回值是否为NULL;
若未做处理则会导致无法回收的有效空间(称为"空洞")出现;
当程序退出后尚未释放的所有动态存储空间将会被归还给系统中的空闲存储池;
即便如此也可能导致系统崩溃(因为这些未释放的空间可能被其他进程占用);
realloc函数允许对已建立的空间块进行重新定位或调整容量大小;
如果要减少已分配的空间容量,则应通过realloc操作来实现这一目标;
当增大已分配的内存时,则可能出现以下几种情况:
- 如果当前内存区域后存在可用空间,则可以直接扩展该区域的空间大小;此时
realloc()函数会返回原有的指针地址。 - 当前区域后的可用空间不足时,则需从堆中寻找第一个满足条件的空闲块(即第一个大小足够大的空闲区域)。此时系统会将现有数据复制到新找到的位置上,并释放原来的数据块;随后函数会返回新的空闲区域起始地址。
- 如果
malloc()无法成功申请到所需的空间,则会导致realloc()函数返回null;但此时原始指针仍然有效并指向原来的资源位置(即未被释放的空间)。
在调用realloc()函数之前必须先记录原始地址,并在必要时通过调用free()函数来释放被占用的空间;否则系统无法保证原有资源的安全性。
第12章 使用结构体和指针
采用结构体与指针作为基础工具时能够构建出功能丰富且适应性强的数据存储方式...其中包含单向链表、双向连接列表以及循环连接列表等基本类型...本章着重讲解有序单向列表与有序双向列表中的插入操作过程
第13章 高级指针
函数指针是指向预先定义好的特定函数的内存地址变量。
通过将多个指向关键点的函数指针存储在一个数组中形成转移表。
在main函数中接收命令行参数的方式有两种:一是通过 argv 参数传递字符数组的引用;二是通过 argc 传递字符数组的数量信息。
当字符串常量用于运算时,默认会被解析为其所占内存空间的起始位置所指向的字符数组。
允许对存储空间进行直接读写操作,并可对内存空间进行间接访问与寻址操作。
第14章 使用预处理器
编译C程序的首要环节是预处理器的作用,其核心内容是在编译源代码之前执行相应的文本替代操作
1、预定义符号
ANSI C中的预定义符号包括:
| 预定义符号 | 含义 |
|---|---|
| FILE | 正在进行编译的文件名 |
| LINE | 文件当前的行号 |
| DATE | 文件被编译时的日期 |
| TIME | 文件被编译时的时间 |
| STDC | 如果编译器支持ANSI,其值为1 |
多种编译器还会设定一些额外的预定义符号;例如,在VC中会设定 _WINVER_ , DEBUG
2、宏定义
仅在文本层面上进行替换操作的宏定义可能会导致运算符优先级出现与预期不符的情况。允许嵌套使用其他类型的宏定义时需注意其对整体结构的影响。在运行时可有效降低堆栈占用量,并从而缩短函数调用的时间。这些特性使得它具备特殊的应用场景和局限性。为了取消某个已声明的宏定义可以在适当的时候使用undef命令。
3、头文件
头文件可以进行嵌套包含,标准要求至少支持8层嵌套。
第15章 输入/输出函数
perror():打印错误信息
exit():终止当前程序的执行
| 函数名称 | 功能 |
|---|---|
| perror() | 打印错误信息 |
| exit() | 终止当前程序的执行 |
| tmpfile() | 创建临时文件 |
| tmpnam() | 创建临时文件名 |
| remove | 删除文件 |
| rename | 对文件进行重命名 |
在标准库中定义的getchar_、putchar_、getC_及putc_这4个函数均以宏形式表示。其对应实现为:#define getchar_ (stdin),#define getc_(stream) fgetc_(stream);其中fgetc_(stream)返回的数据类型是int型数值,则由此可知这些与之相关的接口如getch和putc等也都会按照一致的方式进行处理和实现。
第16章 标准函数库
C语言提供了强大的函数库,包括:
- 算术运算功能模块包括abs()、labs()、div()和ldiv()。
- 随机数生成功能模块包含rand()和srand()两个子模块。
- 字符转码功能模块涵盖了所有字符转码相关操作。
- 双精度计算功能模块支持浮点数的精确运算操作。
- 本地环境处理功能模块集中管理local相关的操作。
- 系统调用管理功能模块负责系统调用的协调与执行。
第17章 经典抽象数据类型
本章主要讲述经典数据类型,栈,队列和二叉树的内容。
