全是随笔,很多是看*《高质量程序设计指南C++》第三版* 的随笔。
C++编译步骤:
hello.c -> 预编译 -> hello.i -> 编译 -> hello.s -> 汇编 -> hello.o -> 链接 -> 可执行文件
-
预编译会对 预编译伪指令(一般以 # 打头,且前面只能出现空白字符)进行处理后 生成中间文件作为编译器的输入。
- #include (头文件的所有内容都会最终合并到某一个或几个源文件中,将所有头文件递归展开后形成的源文件叫做编译单元)
- #define (一般是用对应文本替换)#define 只是简单替换,不做语法检查。(检查留个编译器进行)
- #ifdef
- #pragma
- 构串操作符(# ##), #(变量 生成 字符串), ##(字符串生成变量)
__LINE__ __FILE__
等- 删除所有注释
-
分别对最小编译单元进行编译(以单个头文件所递归包含的所有文件)
- 语法分析:
- 语义分析:
- 中间代码生成
- 汇编:待写入
- 链接:对各个编译单元进行整合,并且链接动态库
- 用户程序调用库(头文件和二进制库组成的库中)接口。连接器会从库中提取相应代码,并和用户程序连接生成可执行文件或动态连接库文件。
运行时: 待写入
OC 编译步骤:
OC
-> 中间代码(.ll)
-> 汇编、机器代码
其中.ll 文件是各平台通用的。
转变命令:clang -emit-llvm -S main.m
长表达式拆分需要在低优先级处拆分为多行,运算符放在新行之首(以示凸出)
if (aaaaaaa > bbbbbbbbbbbb)
&& (ccccccccc > ddddddddd)
&& (eeeeeeeee > fffffffff)
{
pass
}
推荐 以行为为中心
的版式(ADT/UDT)即将public成员写在前面
不推荐 以数据为中心
的版式, 即将private 写在前面
c++ 对 c的最根本改变就是把函数放到了结构当中,从而产生了C++类
动态特性 VS 静态特性
- 静态特性: 程序在编译期就能确定下来的就能确定
- 动态特性: 不是静态特性
- 动态特性是面向对象语言最强大的功能之一, 因为支持可扩展性, 而可扩展性是程序最重要的目标之一。
动态特性:
-
C++:C++虚函数(多态确定调用哪个基类的方法)、 抽象基类(纯虚函数的基类)、 动态绑定、 运行时多态
- 抽象基类:主要用于接口与实现分离,是彻底的封装,创建子类的实例,用基类的对象地址去对外暴露访问。(只发行头文件和二进制文件,保密)(tip: 基类的析构必须是虚析构,否则不能析构子类)
- 基类: 析构函数 必须为虚函数,不然如果子类转基类后,析构不会进入子类的析构函数
- 多态类: 每一个具有虚函数的类叫多态类(虚函数是自己加的或者继承的)。 具体看
# 虚函数表Vtable和 Vptr 一节
不要用数组来直接存放多态对象,而是存放基类指针或者基类的智能指针。 因为存放直接多态对象,会导致每次数组下标查找的是+sizeof(基类的)内存,导致第二个及以后的地址错位。
-
OC: 动态类型(
id
)、多态绑定([obj msgSend]
)、 多态加载(图片2x3x替换,动态加方法和变量)
虚函数表Vtable和 Vptr
- 每一个多态类都有一个或多个 vtable 和 vptr。
- 每一个多态类都有一个虚函数表(vtable),存放着这个类所有的虚函数地址及该类的信息。
- vptr隐含的指针成员(指向当前类的虚函数表)。
- 派生类vtable中所对应的虚函数的位置和基类的位置一样,当前新增的虚函数加到vtable的最后。(这样保持派生类的vtable布局的兼容)
- vtable 和 vptr 必须在init方法的最开始隐式创建并vptr指向vtable。
- 基类调用虚函数的伪代码:
(*(p->_vptr[slotNum]))(p, arg-list)
;p
:基类指针,vptr
: 指p指向的对象的隐含指针,slotNum
: 调用的虚函数在vtable中的编号(编译时就确定下来的)
对象class的内存映像分布
- 成员变量: 用户内存区(包括OC的isa, C++的_vptr)
- 方法(函数): 包括static函数, 代码段(待写入 判断是不是)
- 其他static变量: 程序静态数据区
因此,构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针
struct sizeof
存储变量时地址要求对齐,编译器在编译程序时会遵循两条原则:
- 规则一:结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
- 规则二:结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
struct stru1
{
char b; //start address is 0
short s; //start address is 2 注意这里是2 不是1(根据规则一)
int a; //start address is 0
};
构造函数的成员初始化列表
- 我们一般习惯在构造体函数内来初始化成员数据,这不是真正的初始化,而是赋值,虽然一般来说当成初始化来看。
- 真正的成员初始化是在 “初始化表达式表” 里面,也就是构造函数的{}之前。 这个说明列表里面的初始化工作发生在函数体的任何代码执行之前,编译器也确实是这样做的。
- 初始化列表的顺序不是书写的初始化顺序。而是编译器按照类中的声明顺序来书写初始化列表的。 所以最好调整下顺序
1. 调用基类构造
2. 初始化本类成员(最好按照依赖顺序)
3. 函数体的的其他成员。
对象的构造顺序
- 没有基类很简单,直接初始化就行。
- 如果是派生类对象。
构造流程。是从基类到子类的逐层构造。也可以理解,毕竟虚函数表Vtable和 Vptr
一节得出vtable
总是基类先创建,然后子类拷贝再添加自己的新虚函数
- 析构是自子类到逐层基类的方向调用。(析构顺序严格按照构造相反的顺序调用)
virtual
函数关键字告诉编译器,派生类中相同的成员函数应该放到vtable
中去,并替换基类相应成员函数的位置;
默认构造函数不创建
- 如果没有显示定义默认构造函数,却定义了单参数的构造函数,那么后者会阻止编译器生成前者。于是类就没有了默认构造函数。此时定义该对象就会报错。
struct T
{
T(int){}; //显式创建的构造函数导致 默认构造函数不存在
string p;
};
构造函数和析构函数的调用时机
全局对象在main之前初始化,但是顺序不确定。mian()结束后才析构。
拷贝构造和赋值构造
- 非常容易混淆。
- 拷贝构造:初始化的时候用另一个已经存在的对象来进行进行初始化(初始化时候)
- 赋值构造:只能把对象赋值给一个已经存在的对象(已经创建好的了)
String c= a; //调用拷贝构造,但是风格不好,应该使用 String c(a)
c = a; //赋值
Extern C
void __cdecl foo(int x, int y);
默认用c++ 编译器会 产生的内部名字像 这样:__foo_int_int
的来支持函数重载 。
但是用C编译器 产生的内部名字却为:_foo
.,因为不能重载。
所以需要显式的声明 extern C
, 告诉C++编译器这是C连接函数,并指示连接器到C程序库中去找函数的定义。
函数的重载、覆盖与隐藏
重载
- 具有相同的作用于域(即同一类中)
- 函数名字相同。参数类型、顺序、数目不通过(包括
const
和非const
) virtual
可有可无。
覆盖(override
)
- 不同的作用域。基类和派生类
- 名字相同
- 参数一致
- 基类函数必须是虚函数
隐藏
指派生类成员函数遮蔽了与其同名的基类成员函数。
- 派生类与基类函数同名, 但参数列表有差异。无论有无
virtual
,都将基类函数隐藏。(不是重载) - 派生类与基类同名, 参数列表也相同, 但是基类无
virtual
关键字,基类函数将被隐藏。(不是覆盖)
++ / – 和其重载
使用
例如:++a
, a++
说明:++
前置版本表示先对其执行 +1
, 然后再取值。 ++
后置版本表明先对其 取值运算,再进行+1
。
重载
C++标准规定。当为++ / --
重载运算符时候。
- 重载
++a
, 不需要带参数。Integer & operator++(){}
前置版本 - 重载
a++
, 需要带int参数作为标志(即哑元,非具名参数)。Integer operator++(int){}
后置版本。
用内联函数inline
取代宏
内联和宏的比较
- C++ 的内联设计之初就是用了提高函数的效率。
- C 中可以用宏提高效率。但是宏代码不是函数,只是使用起来像函数。
- 编译预处理用复制宏代码的方式取代函数调用,从而省去了 参数压栈、生成汇编语言的CALL调用、返回参数、执行return的过程,从而提高了效率。
- 宏的缺点是容易出错,比如
#define MAX(a, b) a> b ? a : b
- 宏不能调试,内联函数可以调试。因为
debug
模式并没有展开,可以像普通函数一样调用,release
才真正实施内联。 - 内联可以调试,没有错误的内联函数,声明,名字,类型,返回值类型和函数本地都会放入符号表里面,调用内联函数的时候,编译器会先检查有没有错误,没有就直接替换调用语句。
inine
关键字必须和实现语句放在一起,是用于实现的关键字。
内联不能滥用
以下不适合用:
- 函数体的代码比较长,将使可执行代码膨胀过大
- 函数体内代码循环或者执行时间过长,那么执行时间比调用省去的出栈压栈时间要多的多
memmove 和 memcpy 的区别
- memmove 和直接一个一个替换到原来的内容,可能出现内容的覆盖现象
- memcpy 不会导致内容覆盖现象
容器
std::Vector(向量) 和 linked list(链表)
分别对应了STL的最基本的容器:动态数组和链接表结构
同时也代表了内存存放的两种基本方式: 连续存储和随机存储(不连续存储)
不同的存储方式决定了元素的不同访问方式:随机访问和顺序访问。
随机访问:通过恒定的开销开得到任一元素的内存地址的访问方法
顺序访问:只能从第一个元素开始进行访问
vector
支持随机访问和顺序访问list
支持顺序访问
Tip
- 关联式容器 -> 元素查找比较快
- 顺序容器 -> 插入和删除操作比较快
迭代器iterator
STL容器的有效元素范围,其中迭代器的last要么指向最后一个有效元素的末尾,要么指向一个空白节点,反正不是指向最后一个有效元素,其中遵循了前闭后开的原则,即[first, last)
容量和实际大小
start 到 finish 之间的是有效元素,start 到 end_of_storage 之间的是 总容量, 其中后面没有使用的控件是冗余容量,不属于容器。
多余出来的容量是未经初始化的,只是留待后续元素使用。
可以通过 capacity()
size()
查看容量和元素控件大小。
void reserve(size_type n)
是为容器请求保留的容量的大小,当现有的容器实际大小大于分配的数量的时候,vector
会在自由内存区重新分配一块更大的连续控件,其大小为现有元素的数量n * sizeof(T)
的大小,并将所有的元素从旧位置全部复制到新的位置(调用拷贝构造函数(所以int
等基础类型都会存在拷贝构造函数等,不然放不到容器中去))。- 所以,应该尽量频繁的超出容量大小后往里面添加元素,因为会频繁出发 移动 和拷贝。
STL 容器总结
sizeof VS strlen
char s1[] = "";
sizeof(s1) // 1
strlen(s1) // 0
sizeof()
会加上’\0’的长度
strlen()
遇到’\0’就停止,不会加上’\0’
外平栈 VS 内平栈
http://mallocfree.com/basic/c/c-6-function.htm#79
- __cdecl
- __stdcall
- __fastcall