之前我写过一系列的c/c++ 从汇编上解释它如何实现的博文。从汇编层面上看,确实c/c++的执行过程很清晰,甚至有的地方可以做相关优化。而c++有的地方就只是一个语法糖,或者说并没有转化到汇编中,而是直接在编译阶段做一个语法检查就完了。并没有生成汇编代码。也就是说之前写的c/c++不能涵盖它们的全部内容。而且抽象层次太低,在应用上很少会考虑它的汇编实现。而且从c++11开始,加入了很多新特性,给人的感觉就好像是一们新的编程语言一样。对于这块内容,我觉得自己的知识还是有欠缺了,因此我决定近期重新翻一翻很早以前买的《c++ primer》 学习一下,并整理学习笔记
背景介绍
为什么会想到再次重新学习c++的基础内容呢?目前来看我所掌握的并不是最新的c++标准,而是“c with class” 的内容,而且很明显最近在关注一些新的cpp库的时候,发现它的语法我很多地方都没见过,虽然可以根据它的写法来大致猜到它到底用了什么东西,或者说在实现什么功能,但是要自己写,可能无法写出这种语法。而且明显感觉到新的标准加入了很多现代编程语言才有的内容,比如正则表达式、lambda表达式等等。这些都让写c++变得容易,写出的代码更加易读,使其脱离了上古时期的烙印更像现代的编程语言,作为一名靠c++吃饭的程序员,这些东西必须得会的。
看书、学编程总少不了写代码并编译运行它。这次我把我写代码的环境更换到了mac平台,在mac平台上使用 vim + g++的方式。这里要提一句,在mac 的shell中,g++和gcc默认使用的是4.8的版本,许多新的c++标准并不被支持,需要下载最新的编译器并使用替换环境中使用的默认编译器,使其更新到最新版本
gcc / g++ 使用
在shell环境中,不再像visual studio开发环境中那样,只要点击build就一键帮你编译链接生成可执行程序了。shell中所有一切都需要你使用命令行来搞定,好在gcc/g++的使用并不复杂,记住几个常用参数就能解决日常80%的使用场景了,下面罗列一些常用的命令
- -o 指定生成目标文件位置和名称
- -l 指定连接库文件名称,一般库以lib开头但是在指定名称时不用加lib前缀,例如要链接libmath.o 可以写成-lmath
- -L 指定库所在目录
- -Wall 打印所有警告,一般编译时打开这个
- -E 仅做预处理,不进行编译
- -c 仅编译,不进行链接
- -static 编译为静态库
- -share 编译为动态库
- -Dname=definition 预定义一个值为definition的,名称为name的宏
- -ggdb -level 生成调试信息,level可以为1 2 3 默认为2
- -g -level 生成操作系统本地格式的调试信息 -g相比于-ggdb 来说会生成额外的信息
- -O0/O1/O2/O3 尝试优化
- -Os 对生成的文件大小进行优化
常用的编译命令一般是 g++ -Wall -o demo demo.cpp
开启所有警告项,并编译demo.cpp 生成demo程序
## 基本数据类型与变量 ### 算术类型 这里说的基本数据类型主要是算术类型,按占用内存空间从小到大排序 char、bool(这二者应该是相同的)、short、wchar_t、int、long、longlong、float、double、long double。当然它们有的还有有符号与无符号的区别,这里就不单独列出了 一般来说,我们脑袋中记住的它们的大小好像是固定,比如wchar_t 占2个字节,int占4个字节。单实际上c++ 并没有给这些类型的大小都定义死,而是固定了一个最小尺寸,而具体大小究竟定义为多少,不同的编译器有不同的实现,比如我尝试的wchar_t 类型在vc 编译环境中占2个字节,而g++编译出来的占4一个字节。下面的表是c++ 规定的部分类型所占内存空间大小 | 类型 | 含义 | 最小尺寸 | |:----------|:--------------|:-----| | bool | 布尔类型 | 未定义 | | char | 字符 | 8位 | | wchar_t | 宽字符 | 16位 | | char16_t | Unicode字符 | 16位 | | char32_t | Unicode字符 | 32位 | | short | 短整型 | 16位 | | int | 整型 | 32位 | | long | 长整型 | 32位 | | longlong | 长整型 | 64位 | | float | 单精度浮点数 | 32位 | | double | 双精度浮点数 | 64位 | 另外c++的标准还规定 一个int类型至少和一个short一样大,long至少和int一样大、一个longlong至少和一个long一样大。 ### 有符号数与无符号数 数字类型分为有符号和无符号的,默认上述都是有符号的,在这些类型中加入unsigned 表示无符号,而char分为 signed char、char、unsigned char 三种类型。但是实际使用是只能选有符号或者无符号的。根据编译器不同,char的表现不同。 一般在使用这些数据类型的时候有如下原则 1. 明确知晓数值不可能为负的情况下使用unsigned 类型 2. 使用int进行算数运行,如果数值超过的int的表示范围则使用 longlong类型 3. 算术表达式中不要使用char或者bool类型 4. 如果需要使用一个不大的整数,必须指定是signed char 还是unsigned char 5. 执行浮点数运算时使用double ### 类型转化 当在程序的某处我们使用了一种类型,而实际对象应该取另一种类型时,程序会自动进行类型转化,类型转化主要分为隐式类型转化和显示类型转化。 数值类型进行类型转化时,一般遵循如下规则: 1. 把数字类型转化为bool类型时,0值会转化为false,其他值最后会被转化为true 2. 当把bool转化为非bool类型时,false会转化为0,true会被转化为1 3. 把浮点数转化为整型时,仅保留小数点前面的部分 4. 把整型转化为浮点数时,小数部分为0;如果整数的大小超过浮点数表示的范围,可能会损失精度 5. 当给无符号类型的整数赋值一个超过它表示范围的数时,会发生溢出。实际值是赋值的数对最大表示数取余数的结果 6. 当给有符号的类型一个超出它表示范围的值时,具体结果会根据编译器的不同而不同 7. 有符号数与无符号数混用时,结果会自动转化为无符号数 (使用小转大的原则,尽量不丢失精度) **由于bool转化为数字类型时非0即1,注意不要在算术表达式中使用bool类型进行运算** 下面是类型转化的具体例子 ```cpp bool b = 42; // b = true int i = b; // i = 1 i = 3.14; // i = 3; double d = i; // d = 3.0 unsigned char c = -1; // c = 256 signed char c2 = c; // c2 = 0 gcc 中 255在内存中的表现形式为0xff,+1 变为0x00 并向高位溢出,所以结果为0 ``` 上述代码的最后一个语句发生了溢出,**对于像溢出这种情况下。不同的编译器有不同的处理方式,得到的结果可能不经相同,在编写代码时需要避免此类情况的出现** 尽管我们知道不给一个无符号数赋一个负数,但是经常会在不经意间犯下这样的错误,例如当一个算术表达式中既有无符号数,又有有符号数的时候。例如下面的代码 ```cpp unsigned u = 10; int i = -42; printf("%d\r\n", u + i); // -32 printf("%u\r\n", u + i); //4294967264 ``` 那么该如何计算最后的结果呢,这里直接根据它们的二进制值来进行计算,然后再转化为具体的10进制数值,例如u = 0x0000000A,i = 0xffffffd6;二者相加得到 0xffffffEO, 如果转化为int类型,最高位是1,为负数,其余各位取反然后加一得到0x20,最终的结果就是-32,而无符号,最后的值为4294967264 ### 字面值常量 一般明确写出来数值内容的称之为字面值常量,从汇编的角度来看,能直接写入代码段中数值。例如32、0xff、"hello world" 这样内容的数值 #### 整数和浮点数的字面值 整数的字面值可以使用二进制、8进制、10进制、16进制的方式给出。而浮点数一般习惯上以科学计数法的形式给出 1. 二进制以 0b开头,八进制以0开头,十六进制以0x开头 2. 数值类型的字面值常量最终会以二进制的形式写入变量所在内存,如何解释由变量的类型决定,默认10进制是带符号的数值,其他的则是不带符号的 3. 十进制的字面值类型是int、long、longlong中占用空间最小的(前提是类型能容纳对应的数值) 4. 八进制、十六进制的字面值类型是int、unsigned int、long、unsigned long、longlong和unsigned longlong 中尺寸最小的一个(同样的要求对应类型能容纳对应的数值) 5. 浮点数的字面值用小数或者科学计数法表示、指数部分用e或者E标示 #### 字符和字符串的字面值常量 由单引号括起来的一个字符是char类型的字面值,双引号括起来的0个或者多个字符则构成字符串字面值常量。字符串实际上是一个字符数组,数组中的每个元素存储对应的字符。**这个数组的大小等于字符串中字符个数加1,多出来一个用于存储结尾的\0** 有两种类型的字符程序员是不能直接使用的,一类是不可打印的字符,如回车、换行、退格等格式控制字符,另一类是c/c++语言中有特殊用途的字符,例如单引号表示字符、双引号表示一个字符串,在这些情况下需要使用转义字符. 1. 转义以\开头,后面只转义仅接着的一个字符 2. 转义可以以字符开始,也可以以数字开始,数字在最后会被转化为对应的ASCII字符 3. \x后面跟16进制数、\后面跟八进制数、八进制数只取后面的3个;十六进制数则只能取两个数值(最多表示一个字节) ```cpp '\\' // 表示一个\字符 "\"" //表示一个" "\155" //表示一个 155的8进制数,8进制的155转化为10进制为109 从acsii表中可以查到,109对应的是M "\x6D" ``` 一般来讲我们很难通过字面值常量知道它到底应该是具体的哪种类型,例如 15既可以表示short、int、long、也是是double等等类型。为了准确表达字面值常量的类型,我们可以加上特定的前缀或者后缀来修饰它们。常用的前缀和后缀如下表所示: | 前缀 | 含义 | |:------|:-----------------------| | L'' | 宽字节 | | u8"" | utf-8字符串 | | 42ULL | unsgined longlong | | f | 单精度浮点数 | | 3L | long类型 | | 3.14L | long double | | 3LL | longlong | | u'' | char16_t Unicode16字符 | | U'' | char32_t Unicode32字符 | ## 变量 变量为程序提供了有名的,可供程序操作的内存空间,变量都有具体的数据类型、所在内存的位置以及存储的具体值(即使是未初始化的变量,也有它的默认值)。变量的类型决定它所占内存的大小、如何解释对应内存中的值、以及它能参与的运算类型。在面向对象的语言中,变量和对象一般都可以替换使用 ### 变量的定义与初始化 变量的定义一般格式是类型说明符其后紧随着一个或者多个变量名组成的列表,多个变量名使用逗号隔开。最后以分号结尾。 一般在定义变量的同时赋值,叫做变量的初始化。而赋值语句结束之后,在其他地方使用赋值语句对其进行赋值,被称为赋值。从汇编的角度来看,变量的初始化是,在变量进入它的生命有效期时,对那块内存执行的内存拷贝操作。而赋值则需要分解为两条语句,一个寻址,一个值拷贝。 c++11之后支持初始化列表进行初始化,在使用初始化列表进行初始化时如果出现初始值存在精度丢失的情况时会报错 c++11之后的列表初始化语句,支持使用赋值运算幅、赋值运算符加上{}、或者直接使用{}、直接使用() ```cpp int i = 3.14; //正常 int i(3.14); //正常 int i{3.14}; //报错,使用初始化列表进行初始化时,由double到int可能会发生精度丢失 int i(3.14); //正常 ``` 如果变量在定义的时候未给定初始值,则会执行默认初始化操作,全局变量会被赋值为0,局部变量则是未初始化的状态;它的值是不确定的。这个所谓的默认初始化操作,其实并不是真的那个时候执行了什么初始化语句。全局变量被初始化为0,主要是因为,在程序加载之初,操作系统会将数据段的内存都初始化为0,而局部变量,则是在进入函数之后,初始化栈,具体初始化为何值,根据平台的不同而不同 ### 声明与定义的关系 为了允许把程序拆分为多个逻辑部分来编写,c++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。 如果将程序分为多个文件,则需要一种在文件中共享代码的方法。c++中这种方法是将声明与定义区分开来。在我之前的博客中,有对应的说明。声明只是告诉编译器这个符号可以使用,它是什么类型,占多少空间,但前对它执行的这种操作是否合法。最终会生成一个符号表,在链接的时候根据具体地址,再转化为具体的二进制代码。而定义则是真正为它分配内存空间,以至于后续可以通过一个具体的地址访问它。 声明只需要在定义语句的前面加上extern关键字。如果extern 关键字后面跟上了显式初始化语句,则认为该条语句是变量的定义语句。变量可以声明多次但是只能定义一次。另外在函数内部不允许初始化一个extern声明的变量 ```cpp int main() { extern int i = 0; //错误 return 0; } ``` 一个好的规范是声明都在放在对应的头文件中,在其他地方使用时引入该头文件,后续要修改,只用修改头文件的一个地方。一个坏的规范是,想用了,就在cpp文件中使用extern声明,这样会导致声明有多份,修改定义,其他声明都得改,项目大了,想要找起来就不那么容易了。 ### 变量作用域 变量的作用域始于声明语句,终结于声明语句所在作用域的末端 1. 局部变量在整个函数中有效 2. 普通全局变量在整个程序中都有效果 3. 花括号中定义的变量仅在这对花括号中有效 作用域可以存在覆盖,并且以最新的定义的覆盖之前的 ```cpp int i = 10; void func() { int i = 20; { string i = "hello world"; cout << i <
评论 (0)