目录
一、基础之查漏补缺
1.float精度问题
2.字符型数据
3.变量初值问题
4.赋值&初始化
5.头文件之<> VS " "
6.逻辑运算
7.数组
7.1 二维数组初始化
7.2 字符数组
8.字符串处理函数
8.1 strcat
8.2 strcpy
8.3 strcmp
8.4 strlen
9.函数
9.1 形参
9.2 全局变量
9.3 变量存储方式
(1)存储空间
(2)动态存储区中存储哪些数据呢?
(3)静态存储区中存储哪些数据呢?
9.4 static关键字用法
10. 编译预处理
10.1 编译
10.2 预处理
10.3 宏 VS 函数
11.结构体&共用体
11.1 结构体
11.2 共用体
12.位运算
12.1 位运算的应用
13.文件
13.1 文件分类
13.2 文本文件的几个特点
13.3 文本文件缺点
13.4 fopen函数
13.4 fputc & fgetc函数
13.5 feof函数
13.6 \r\n
二、调试技巧
1.内存查看
一、基础之查漏补缺
1.float精度问题
当把一个十进制数值赋给一个实型变量时,计算机会把该十进制数转换成二进制数保存,当程序执行流程停在断点上,用鼠标查看该变量值时,计算机实际上是把它保存的二进制数再转换成十进制数显示出来,这个步骤——“十进制→二进制,二进制→十进制”中,存在着一些除法运算,这些除法运算因无法整除的原因,会导致从二进制转换回十进制数时丢失精度。
2.字符型数据
将一个字符常量放到一个字符变量中,实际上并不是把字符本身放到字符变量所属的内存中,而是把字符对应的ASCII码(一个数字)存放到内存中。
什么是ASCII码?通俗地说,就是范围处于0~127之间的一个整数(数字)。例如,字符a对应的数字是97,b对应的数字是98。
既然在内存中,字符数据是以ASCII码存储,说明字符型数据的存储形式和整数的存储形式类似。所以,在C语言中,字符型数据和整型数据之间可以互通使用,一个字符数据既可以以字符形式输出,又可以以整数形式输出,以字符形式输出时,计算机会先将内存中的ASCII码转换成相应的字符,然后输出。
字符'a'在内存中占1字节,而"a"在内存中占2字节。"a"的最后一个字符为'\0',这是一个转义字符,也就是说,"a"其实是由两个字符构成,但是'\0'如果出现在printf中进行输出,却并不会输出出来,而是作为字符串结束标记来标记这个字符串内容结束。
在写一个字符串常量时,不要手工去增加'\0',这是画蛇添足,'\0'是系统增加的。
3.变量初值问题
C语言中,定义变量时,不赋初值的变量中所保存的值是不确定的,所以,不赋初值的变量,不应该拿来参与运算。
对比C#,规则就不一样了。在C#中,变量的默认值是由其类型决定的。对于整型变量(如int),如果只是声明了它而没有给它赋值,那么它的默认值会自动设为0。
4.赋值&初始化
下面这行代码不是赋值语句,称为定义时初始化(定义时给初值)语句更为合适,因为这行代码最前面有一个类型标识符char(赋值语句是不可以以类型名作为语句开头的):
int a = 10;
5.头文件之<> VS " "
用尖括号<>括起来的头文件被#include时,表示让Visual Studio去系统目录中寻找.h文件,所以一些系统提供的标准头文件如stdio.h、stdlib.h等在#include时都应该使用尖括号<>括起来。
而用双引号" "包含起来的头文件被#include时,Visual Studio会优先在当前源代码文件所在的目录下寻找,如果找不到,再到系统目录中寻找,所以,通常开发者自己写的一些头文件,在被#include包含进来时,往往使用双引号""包含起来。
6.逻辑运算
很多逻辑表达式只需要计算其中的一部分内容,就可以得到整个逻辑表达式的值,这种逻辑表达式的求值特性,也称为逻辑表达式的“短路求值”特性(只要最终的结果已经可以确定是真或假,求值过程便宣告终止)。
7.数组
7.1 二维数组初始化
C语言二维数组定义得一般格式:类型说明 数组名[常量表达式][常量表达式],eg:
int a[3][4]; //不能把两个中括号写成一个,a[3,4]是错误的写法
C#中二维数组的声明和初始化:
int[,] array = new int[3, 4] {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
};
7.2 字符数组
字符数组中不但能保存字符,还能保存字符串。只要用字符串来初始化字符数组(推荐),系统就会自动在字符串末尾加一个'\0'。
char str[6] = "Hello"
所以如果要使用给字符数组中每个元素分别赋值的方式(不过这种写法很罕见,不推荐每个元素分别赋值的方式)对字符数组进行初始化,建议和字符串保持一致,也就是人为地增加一个'\0',这样做主要是为了确定字符串的实际长度(因为字符串实际长度是靠找到末尾的'\0'来确定的)。
8.字符串处理函数
8.1 strcat
strcat(字符数组1,字符数组2);
连接两个字符数组中的字符串,把字符数组2的内容连接到字符数组1后边,结果存放在字符数组1中。
- 字符数组1必须足够大,能够容纳连接后的新字符串。
- 连接之前两个字符串后面都有一个\'0',连接时将字符串1末尾的\'0'删除并开始连接字符串2的内容,连接后只在新字符串的末尾保留一个'\0'。
- 连接后str2内容不发生任何变化,换句话说,strcat对str2没有任何影响。
8.2 strcpy
strcpy(字符数组1,字符串2);
将字符串2复制到字符数组1中。字符数组1中的内容将被覆盖。
- 字符数组1必须足够大,以便能容纳下被复制的字符串,也就是说,字符数组1的容量不能小于字符串2的长度(不要忘记字符串2末尾还有个'\0'也要占一个位置)。
- 字符数组1必须是一个数组名,而字符串2可以是一个数组名,也可以是一个字符串常量。
- 复制的时候连同字符串末尾的'\0'也一起复制到字符数组中。
- 不能用赋值语句将一个字符串常量或者字符数组名直接赋给一个字符数组(字符数组名),必须得用strcpy函数。赋值语句只能将一个字符赋给一个字符型变量或数组元素
- 复制后str2内容不发生任何变化,str1中虽然有两个\'0',但是当输出该字符串内容时,只输出到第一个'\0'之前。
8.3 strcmp
strcmp(字符串1,字符串2);
比较字符串1和字符串2中的内容,这也是一个比较常用的函数:
- 如果字符串1=字符串2,则函数返回0。
- 如果字符串1>字符串2,则函数返回一个正整数。
- 如果字符串1<字符串2,则函数返回一个负整数。
比较规则:对两个字符串自左至右逐个字符比较(按ASCII码值大小比较),一直到出现不同的字符或者遇到'\0'为止,若全部字符相同,认为相等,若出现不相同的字符,则以第一个不相同的字符比较结果为准。
一般来说,strcmp常用于比较两个字符串是否相等,而比较大小则用的比较少,因为比较大小一般来说意思不大。
8.4 strlen
strlen(字符数组);
得到字符串长度,本函数执行的结果值为字符串的实际长度,但不包括字符串结束标记'\0'。
9.函数
系统会给函数调用分配一些内存来保存一些信息(局部变量、函数参数、函数调用关系等)
9.1 形参
(1)调用函数时,会为函数的形参分配内存,函数调用结束后,形参的内存会被释放,所以形参只能在函数内部使用。
(2)形参数组大小可以不指定,即便指定了也可以与实参数组大小不一致,因为C编译器对形参数组大小不做检查,只是将实参数组的首地址传递给形参数组,甚至可以定义形参数组大小比实参数组大,但超过实参数组大小的部分内存不要去引用,否则会导致程序立即或者不定时崩溃。
(3)可以用多维数组名作为形参和实参。形参数组在定义时,可以指定每一维的大小,也可以省略第一维的大小,但不能省略第二维的大小。请记住一点:实参是多少行多少列,形参就尽量跟实参一样(也是这些行这些列),这样实参能引用的下标形参一样能引用,就会保证写的代码不出错误。
(4) 形参是局部变量。
9.2 全局变量
从变量的作用域角度来划分,可分为局部变量和全局变量。
(1)全局变量的优缺点:
*优点:
增加了函数与函数之间数据联系的渠道。如果一个函数中改变了全局变量的值,就能影响到其他使用到该全局变量的函数,相当于在各个函数之间有了直接的传递数据的通道,不需要再通过实参、形参来传递数据了。因为C语言中函数只能返回一个值(无法一次返回多个值),所以如果使用了全局变量,也就相当于能够从函数中返回多个值了。
*缺点:
- 只在必要的时候才使用全局变量(谨慎使用),因为全局变量在程序整个执行期间一直占用着内存,而不像函数内的局部变量,当函数执行完毕后,这些局部变量所占的内存会被系统释放(回收)。
- 降低了函数通用性,因为函数执行时可能要依赖这些全局变量,如果将函数迁移到另一个源程序文件中时,与该函数相关的全局变量也需要考虑迁移问题,并且如果迁移到的目标源程序文件中也有同名的全局变量,就比较麻烦。
- 到处是全局变量,降低了程序的清晰性和可读性。读程序的人难以清楚地判断每个瞬间每个全局变量的值(因为很多函数都能改变该全局变量的值)。
(2)如果某个函数想引用在它后面定义的全局变量,可以使用关键字extern做一个“外部变量说明”,表示该变量在函数的外部定义,这样在函数内就能使用,否则编译就会出错,但有一点要注意,全局变量在定义的时候是可以给初值的,但是在做外部变量说明时,是不可以给变量初值的。
int main()
{
extern int value;
std::cout << "value:" << value << std::endl;
}
int value = 20;
(3)在同一个源文件中,如果全局变量和局部变量同名,则在局部变量作用范围(作用域)内,全局变量不起作用,如果给局部变量赋值,当然也不会影响全局变量的值。
(4) 如果在一个源程序文件中定义的全局变量想在该项目的其他源程序文件中使用,则只需要在其他的源程序文件中使用上面介绍的extern关键字做外部变量说明,就可以在其他源程序文件中使用该全局变量了。
一般来说,一个全局变量的作用域是从它定义的点到整个源程序文件结束,但是,通过使用extern,将它的作用域扩大到了有extern说明的其他源程序文件。如果再有更多的源程序文件中要引用这个全局变量,也要在这些源程序文件的开头用extern来说明这个全局变量为外部全局变量。
希望某些全局变量,只能在本源程序文件中被使用,不想被其他源程序文件进行跨文件引用,那也很简单,在定义这个全局变量时在最前面加上static关键字。
9.3 变量存储方式
如果从变量存在的时间(生存期)角度来划分,变量可以划分为“静态存储变量”和“动态存储变量”,从而就引出了“静态存储方式”和“动态存储方式”。
- 静态存储方式:在程序运行期间分配固定的存储空间的方式。
- 动态存储方式:在程序运行期间根据需要进行动态的分配存储空间的方式。
(1)存储空间
存储空间分成三个主要部分:程序代码区、静态存储区和动态存储区。程序执行所需的数据就放在静态存储区和动态存储区中,存储区就理解成内存。
(2)动态存储区中存储哪些数据呢?
- 函数形参,前面说过,函数形参被看作局部变量。
- 局部变量,如函数内定义的一些变量。
- 函数调用时调用现场的一些数据和返回地址等。
一般来说,这些数据在函数调用开始时分配存储空间,函数调用完毕后这些空间就被释放掉了(也称为回收)。这种分配和释放就认为是动态的,如果两次调用同一个函数,分配给此函数的局部变量的存储空间地址可能是不同的。
(3)静态存储区中存储哪些数据呢?
全局变量(包括全局静态变量,在函数的外部定义的)放在静态存储区中,程序开始执行时给全局变量分配存储区,程序执行完毕后释放这些存储区。在程序执行过程中它们占据固定的存储单元,而不是动态地分配和释放。
有时希望函数中局部变量的值在函数调用结束后不消失(不被系统自动释放)而保留原值,也就是说,它占用的存储单元不释放,在下一次调用该函数时,该变量中保存的值就是上一次该函数调用结束时的值,这是可以做到的,只需指定该局部变量为“局部静态变量”,用static关键字加以说明即可。局部静态变量:
- 在静态存储区中分配存储单元,程序整个运行期间都不释放。·
- 局部静态变量是在编译时赋初值的,只赋初值一次,在程序运行的时候它已经有了初值,以后每次调用函数时不再重新赋初值,只是保留上次函数调用结束时的值,而普通变量的定义和赋值是在函数调用时才进行的。
- 定义局部静态变量时如果不赋初值,则编译时自动给其赋初值0,而常规变量,如果不赋初值,则它是一个不确定的值。
- 虽然局部静态变量在函数调用结束后仍然存在,但在其他函数中是不能引用的。
- 局部静态变量长期占用内存,降低了程序可读性(当多次调用该函数时往往弄不清当前该静态变量的值是多少)。
所以得到一个结论:如非必要,不要过多使用局部静态变量。
9.4 static关键字用法
(1)函数内部在定义一个局部变量时,在前面使用static关键字,则该变量会保存在静态存储区,在编译的时候被初始化,如果不给初始值,它的值会被初始化为0,并且,下次调用该函数时该变量保持上次离开该函数时的值。
(2)在定义全局变量时前面使用static关键字,那么该全局变量只能在本文件中使用,无法在其他文件中被引用(使用)。
(3)在函数定义之前增加static,那么该函数只能在本源程序文件中调用,无法在其他源程序文件中调用。
10. 编译预处理
一个项目,由一个或者多个源程序文件组成,可以通过编译、链接(Visual Studio负责做这件事)最终生成一个可执行文件。
10.1 编译
编译,是以一个一个的源程序文件(.cpp文件)为单位进行的,每个源程序文件都会编译成一个目标文件(目标文件扩展名可能是.o也可能是.obj等,这与操作系统类型有关),如果源程序文件有多个,则会编译生成多个目标文件,然后将这些目标文件进行链接,最终生成一个可执行文件。
一般来说,编译阶段会做如下几件事:
- 预处理。
- 编译。包括词法分析、语法分析、目标代码生成、优化等。
- 汇编。产生.o(.obj)目标文件。
10.2 预处理
C语言一般提供三种预处理功能:宏定义、文件包含、条件编译。这三种预处理功能也是通过在源程序文件写入代码来实现的,只不过这些代码比较特殊,都是以“#”开头。
宏定义其实并不是C语言语句(虽然有时候会称其为语句),不必在行末加分号,如果加分号则连分号一起被替换了。
10.3 宏 VS 函数
- 用宏的次数如果增多,源程序代码就会增多,但函数调用不会使源程序代码增多
- 宏展开只占用编译时间,不占用运行时间,而函数调用占用运行时间(分配内存、传递参数、执行函数体、返回值等)
11.结构体&共用体
11.1 结构体
在 C 语言中,声明结构体变量时必须包含"struct"关键字(除非用typedef创建了新类型),而在 C++ 中则不必。
struct Person {
char name[50];
int age;
};
struct Person person;
strcpy(person.name, "Alice");
person.age = 20;
11.2 共用体
结构体占用的内存大小是各个成员占的内存大小之和,每个成员分别占用一段不同的内存。共用体因为成员占用同一段内存,所以占用的内存大小等于占用内存最大的成员所占的内存大小,而不是每个成员所占内存大小之和。
共用体变量地址和其成员的地址都相同。共用体变量名也代表共用体变量的首地址,这一点与数组名代表数组首地址的说法类似。
共用体变量不能在定义的时候给所有成员都进行初始化,但是在定义的时候初始化第一个成员是允许的。
12.位运算
1字节由8个二进制位组成,最左边的位称为最高位,最右边的位称为最低位,每个二进制位的值是0或者1(二进制数,只有0和1两个数字,不能是其他数字)。
1字节能表示的数字范围,如果用二进制数来表示,能表示的最大二进制数是11111111(十进制数255),最小二进制数是00000000(就是0)。1字节能表示8个二进制位,4字节呢?显然,能表示32个二进制位。等号左侧是二进制数(32个二进制位),等号右侧是十进制数:
11111111,11111111,11111111,11111111=4294967295
所以,十进制数的4294967295就是unsignedint类型能够表示的最大数字。
12.1 位运算的应用
如果某个数字的某些二进制位想翻转(从0变成1,从1变成0),那这个位可以和1做异或运算,如果某些二进制位想保持不变,那这个位可以和0做异或运算。
左移运算符“<<”将一个数的二进制位左移若干位,右侧补0,每左移一位都相当于把原来的数字乘以2。
右移运算符“>>”将一个数的二进制位右移若干位,超出最低位的被舍弃,左侧高位补0,每右移一位都相当于除以2。
13.文件
13.1 文件分类
根据数据组织形式,把文件分为两种:文本文件(ASCII文件)与二进制文件。
- 文本文件:也称为ASCII文件,文件中的每个字节存放一个ASCII码,代表一个字符,这种文件在打开后能够直接看懂其中的内容。
- 二进制文件:把内存中的数据按照其在内存中的存储形式原样输出到磁盘上存放。这种文件中一般会有很多不可见字符,打开后看到的可能是一堆乱码。
13.2 文本文件的几个特点
- 文本文件中,每字节存放一个ASCII码,代表一个字符。
- 文本文件中的内容人类能够看懂。
- 一个整数10000,按照文本文件格式保存,通过观察图12.5,是占5字节。
13.3 文本文件缺点
- 众所周知,10000是一个整型数,在计算机中该数字用short int保存就足矣,short int只占2字节,但是在文本文件中保存却需要5字节,所以这种保存形式占用的存储空间比较大。
- 当双击一个文本文件时,或者用二进制编辑器打开文本文件时,系统都显示出来了人类能够读懂的文本内容。(前面谈过,文本文件或者二进制文件是对于人类来说的,而对于计算机,并不区分是什么类型的文件,保存的都是二进制数据)所以,当用文本形式打开文件时,系统会多做一个工作,就是把二进制数据转换成人类能看懂的ASCII码数据。
13.4 fopen函数
文件在进行读或者写之前,必须要先打开,在读或者写结束之后,必须要关闭,否则会造成资源泄漏或读写失败。文件的打开要调用fopen函数。
FILE* fp = fopen("", "r");
FILE结构体:每次用fopen函数打开一个文件,系统都会开辟出一块内存,这块内存大小是sizeof(FILE),这块内存用来存放和文件相关的信息,诸如文件名、文件使用方式、当前文件位置等。通过调用fopen函数,可以告诉系统三个信息:(1)需要打开的文件名。(2)文件使用方式,如是读还是写。(3)让哪个指针变量指向被打开的文件。
每个打开的文件都有一个当前位置指针,其实就是保存在FILE结构里的一个char*型的字符指针(不同版本编译器可能细节不同,但道理都相同)。这个位置指针的用途就是代表当前从文件的哪个位置开始读/写数据,对于读来讲,每读出1字节数据,这个位置指针会自动往后移动1字节,以指向下一字节,这样,下次再读时自然就从下一字节开始。
13.4 fputc & fgetc函数
fputc函数用于把一个字符写到磁盘文件。fgetc函数用于从指定文件读入一个字符。这两个函数如果执行失败或者整个文件读到末尾,则返回EOF。
EOF是EndOfFile(文件末尾)的缩写,是系统提供的一个宏定义,代表-1。可用EOF(-1)来判断读入的内容是否到达文件结束。但一旦该文件中真存在一个值为-1的字符(该字符的十六进制是FF,用fgetc读入进来就是-1),那么,用EOF这种判断方式来判断是否读到文件结束,就会出现错误,所以需要换一种范例写法,引入feof函数。
13.5 feof函数
用来判断文件是否结束(文件当前位置指针是否指向文件末尾)。如果文件结束,则返回1;如果文件没结束,则返回0。
while(!feof(fp))
{
//...
}
不管使用fopen函数时是以什么样的文件使用方式打开文件(例如用r还是用rb都没关系),feof函数都能够正确地判断文件是否结束。
13.6 \r\n
在Windows环境下,每个文本行的末尾都有两个二进制数——“0D 0A”,通过查询完整的ASCII码表,不难发现,0D代表回车符,0A代表换行符。即:文本文件的每一行通常都以 \r\n 结束。然而,当使用 fgets() 读取文件时,它只会把 \n 作为行结束符,并且也会被存储到缓冲区中。
在Windows平台,如果打开文件是用于读,则不带"b"标记可能会使读出的字符有所缺失,如可能缺失了'\r'。如果打开文件是用于写,则不带"b"标记可能会将一些额外的字符写入文件(本来只想写入'\n',实际写入的却是'\r'和'\n')。所以,如果希望把看到的内容原封不动地写入文件中,请在fopen函数的文件使用方式参数中增加"b"标记来辅助;如果希望把文件中的内容原封不动地读出来,请在fopen函数的文件使用方式参数中增加"b"标记来辅助。
二、调试技巧
1.内存查看
在断点调试时,选择“调试”→“窗口”→“内存”→“内存1”命令,就打开了“内存”查看窗口,只需要在其中输入内存地址,就可以看该地址所对应的内存中内容。
双击aaa变量名,直接按住鼠标左键往左上角“地址”右侧的编辑框中拖动,此时,变量aaa所代表的内存地址中的内容便显示到“内存”查看窗口中。
“内存”查看窗口中,左上角的“地址”部分显示的0x0039FA8C是数组aaa所代表的内存地址。往下看,分成三部分:
- 左侧部分显示的内存地址,是aaa的地址和紧邻的内存地址;
- 中间部分显示的是内存地址中保存的十六进制数字内容(内存中保存的数据都是二进制数据,但为了方便观察,Visual Studio把这些二进制数据以十六进制形式显示出来,四位二进制数字显示成一位十六进制数字);
- 右侧部分显示的是内存中的十六进制数字所代表的一些可显示字符,从中可以找到"123456789a"字样,通过逐个字符比较,可以看到,'\0'这个转义字符在内存中显示的十六进制数字是00,其他的字符,'a'字符在内存中显示的十六进制数字是61,十六进制数字61正好对应十进制数字97,而十进制数字97正好就是字符'a'的ASCII码,所以在内存中存放一个字符时,存放的其实就是该字符的ASCII码。