C++ 学习记录随笔

全是随笔,很多是看*《高质量程序设计指南C++》第三版* 的随笔。

C++编译步骤:

hello.c -> 预编译 -> hello.i -> 编译 -> hello.s -> 汇编 -> hello.o -> 链接 -> 可执行文件

  1. 预编译会对 预编译伪指令(一般以 # 打头,且前面只能出现空白字符)进行处理后 生成中间文件作为编译器的输入。

    • #include (头文件的所有内容都会最终合并到某一个或几个源文件中,将所有头文件递归展开后形成的源文件叫做编译单元)
    • #define (一般是用对应文本替换)#define 只是简单替换,不做语法检查。(检查留个编译器进行)
    • #ifdef
    • #pragma
    • 构串操作符(# ##), #(变量 生成 字符串), ##(字符串生成变量)
    • __LINE__ __FILE__
    • 删除所有注释
  2. 分别对最小编译单元进行编译(以单个头文件所递归包含的所有文件)

  • 语法分析:
  • 语义分析:
  • 中间代码生成
  1. 汇编:待写入
  2. 链接:对各个编译单元进行整合,并且链接动态库
  • 用户程序调用库(头文件和二进制库组成的库中)接口。连接器会从库中提取相应代码,并和用户程序连接生成可执行文件或动态连接库文件。

运行时: 待写入

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值