C++复习

面向对象

面向对象的三大基本特性: 封装、 继承、 多态
此外, 还要知道 C与C++的区别, 面向对象与面向过程的优缺点 ,类和结构体区别, C和C++结构体区别。

类的定义

具有相同属性和行为的个体的抽象,个体就是对象,相当于变量。
对象的特点:

  1. 属于某个实例
  2. 唯一(地址唯一)
  3. 具有属性和行为
  4. 行为能改变属性;抽象就是类,实例就是对象
  5. 类由成员函数成员属性组成
    类的成员属性访问属性:pulic;protected;pricate;默认为private
    结构体默认public考虑在不同情况下(1)继承 (2)子类 (3) 除子类, 类外

内部组成

构造函数: 与类名相同; 无返回值.
种类: 无参构造, 有参构造, 拷贝构造. 当没有任何构造时, 系统有默认无参构造.有任何构造, 就没有默认无参了.有其他构造, 没有自己的拷贝构造, 系统也有默认拷贝构造, 只有存在自己的拷贝构造, 系统才不会给你生成一份. 初始化列表, 用来初始化成员属性. 初始化列表, 还会初始化父类, 初始化对象 , 初始化引用和初始化const 属性.初始化列表先于构造函数执行. 构造函数会对虚表隐式赋值.
拷贝构造, 当用一个类对象来初始化另一个对象的时候调用拷贝构造. 默认拷贝构造是浅拷贝, 不同对象, 共用空间, 如果一个对象对空间进行了释放, 另一个再操作该空间, 会造成程序崩溃.需要使用深拷贝, 不同对象使用自己的那一份.

拷贝构造使用的几种情况:

  1. 传参数, 参数在对象;
  2. 返回值为对象 ;
  3. 用一个对象初始化另一个对象.
    拷贝构造的例子
    B Play(B b)
    {
    return b;
    }
    那么执行的过程是 先是拷贝构造, 给b初始化, 然后利用b拷贝构造初始化临时对象, 然后销毁b , 然后返回临时对象, 然后销毁临时对象.
    初始化列表在初始化时,按照成员变量的顺序初始化.

析构函数.

无参 ~类名称 无返回值 只有一个 ,默认属性public. 对指针类型的成员属性, 进行回收操作. 如果是作为父类角色出现, 要考虑使用虚析构

const 常函数

const 可以修饰属性, 函数, 对象. 修饰函数, 关于const关键字, 函数内不能改变属性的值, 因为函数属性内有隐藏参数const this *. 一般对象可以调用所用类型函数包括常函数, 常对象只能调用常函数.const 属性要在初始化列表中初始化.

static修饰词.

static属性, 类一份, 无法继承, 对象共用, 不依赖对象存在. static 函数, 类共用, 无法继承, 不能是虚函数, 不依赖对象存在, 调用时, 可以使用类名作用域, 指针和对象. 函数内只能使用静态属性. static 对象, 与之前的静态变量时一样的用法.

内联函数

inline修饰的函数. 用于代码少, 调用频繁的场合. 在调用处展开, 用空间换时间.

友元函数.

友元函数, 友元类. 并不是类内的函数, 是类外的全局函数, 或是其他类中的函数. 定义成友元后, 可以使用类内所有成员, 包括私有成员. 关键字 friend. 由于并不是类的成员函数, 那么在使用该类的成员时, 依赖对象.

虚函数,

用virtual 修饰的函数, 用于虚函数多态.

纯虚函数,

虚函数=0 , 含有纯虚函数的类, 抽象类, 全是纯虚函数的类是接口类, 含有纯虚函数, 要求派生类, 完成所有纯虚方法后, 方可定义对象.

操作符函数

系统默认的重载操作符函数有 operator = 和 &
默认构造;析构; 拷贝构造 ; =赋值运算符; &取址运算符;

类与类之间的关系:

类与类之间的关系有:组合, 依赖, 聚合, 关联, 继承

组合

部分与整体, 是组成的一部分 , 代码体现为成员属性是对象. 包含生命期的制约

依赖

完成功能的必备工具. 代码体现为函数参数为类对象引用,或类对象, 或函数内new 对象

聚合

所属关系, 很多实体归一个组织来管理. 代码体现是指针链表或者指针数组, 并附有生命周期制约
关联: 有没有, 代码体现上是指针属性
继承: 父类和子类之间的关系, 父类所有属性和方法会复制一份给子类.通过继承方式公有, 保护, 私有继承来完成继承关系.
在公有继承下,
public ->public protected ->protected private-> 不可访问
在保护继承下,
public->protected protected -> protected private ->不可访问
在私有继承下,
public->private protected->private private->不可访问

重写(override)与重载(overload)的区别.

重写,又叫做覆盖, 子类重新定义父类中有相同名称和参数的虚函数. 函数特征相同。但是具体实现不同,主要是在继承关系中出现的. 重写需要注意:

  1. 被重写的函数不能是static的。必须是virtual的
  2. 重写函数必须有相同的类型,名称和参数列表,返回值一般相同(重写中协变除外, 父类返回父类指针, 子类返回子类指针)
  3. 重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的.
    重载, 是函数名相同,参数列表不同; 重载只是在类的内部存在。但是不能靠返回类型来判断

多态

静态多态

编译期而非运行期, 实现方法为运算符重载 \ 函数重载 \ 带变量的宏多态

动态多态

通过类继承机制和虚函数机制生效于运行期, 实现方式为虚函数—父类指针指向子类空间 , 通过父类指针调用虚函数.

  1. 父类中虚函数, 父类会有一个保存虚函数的虚表(虚函数地址数组).子类继承父类, 那么子类会从父类那里得到一份从父类复制的虚表
  2. 子类重写父类虚函数, 子类的虚函数会覆盖虚表中父类的虚函数地址
  3. 定义子类对象, 在子类的前四个字节会有一个虚指针vfptr, 指向子类的虚函数表 ,父类指针指向子类的对象 4)通过父类指针调用虚函数, 那就会在地址找到对象的前四个字节, 得到虚表的入口地址, 然后遍历虚表, 找到对应的虚函数, 完成虚函数多态.

虚函数和普通函数最后执行哪个?

class A
{
public : 
	void fun(){printf("this is A\n");};
};
class B : public A
{
public :
	 void fun(){printf("this is B\n");};
};
int main()
{
		A a;
		B b;
		a.fun();
		b.fun();	
		B* pb = (B*) &a;
		pb->fun();
		A *pa = &b;
		pa->fun();
		return 0;
}

输出结果:

this is A
this is B
this is B
this is A
class A
{
public : 
	virtual void fun(){printf("this is A\n");};
};
class B : public A
{
public :
	virtual void fun(){printf("this is B\n");};
};
int main()
{
		A a;
		B b;
		a.fun();
		b.fun();	
		B* pb = (B*) &a;
		pb->fun();
		A *pa = &b;
		pa->fun();
		return 0;
}

输出结果:

this is A
this is B
this is A
this is B

sizeof(A) = 4
除了虚函数 , 父类指向子类空间调用子类的, 其余的就是什么类型对象就调用什么类型的 ,父类指针,那么就调用父类, 子类对象, 就调用子类的成员

class CBase
{
public:
	CBase()
	{

	}
	virtual /*CBase**/void AA()
	{
		cout<< "CBase::AA"<<endl;
		/*return 0;*/
	}
	virtual void BB()
	{
		cout<< "CBase::BB"<<endl;
	}
	void CC()
	{
		cout<< "CBase::CC"<<endl;
	}
};

class CDerived: public CBase  //继承方式默认私有
{
public:
//protected:
	virtual /*CDerived**/void AA() override
	{
		cout<< "CDerived::AA"<<endl;
		/*return 0;*/
	}
	void CC()
	{
		cout<< "CDerived::CC"<<endl;
	}
	virtual void DD()
	{
		cout<< "CDerived::DD"<<endl;
	}
};

class CKKKK
{
public:
	virtual void AA()
	{
		cout<< "CKKKK::AA"<<endl;
	}
};
class CQQQQ
{
public:
	void AA()
	{
		cout<< "CQQQQ::AA"<<endl;
	}
};
int main()
{
	*CBase*/CDerived * pBase = new CDerived;
	pBase->BB();
	static_cast<>(); 强制类型转换  (type) 转换要合理, 否则不过编译   dynamic_cast<>();
	//虚函数和非虚函数的调用
	CBase *pBase = new CBase;
	pBase->AA(); // CBase::AA
	pBase->BB(); // CBase::BB
	pBase->CC(); // CBase::CC
	cout << "=============================="<<endl;
	static_cast<CDerived*>(pBase)->AA();// CBase::AA
	static_cast<CDerived*>(pBase)->BB();//CBase::BB
	((CDerived*)pBase)->CC();//子类::CC
	cout << "=============================="<<endl;
	((CKKKK*)(pBase))->AA(); // CBase::AA
	((CQQQQ*)(pBase))->AA(); // CQQQQ::AA

	cout << "=============================="<<endl;
	pBase = new CDerived;
	pBase->AA();//子类::AA
	pBase->BB();//父类::BB
	pBase->CC();//父类::CC
	cout << "=============================="<<endl;
	static_cast<CDerived*>(pBase)->AA();//子类的AA
	static_cast<CDerived*>(pBase)->BB();//父类的BB
	((CDerived*)pBase)->CC();//子类::CC
	cout << "=============================="<<endl;
	((CKKKK*)(pBase))->AA();//子类的AA
	((CQQQQ*)(pBase))->AA();//QQQQ的AA
	cout << "=============================="<<endl;
	return 0}

类空间大小的计算

空类 sizeof() = 1 static 属性, 不占空间, sizeof()不做计算. 虚函数多虚指针, sizeof() +4 .

虚继承

虚继承问题

虚继承的背景知识: 普通继承关系中, 子类的虚表是从父类拷贝过来的.子类新增的特有虚函数, 添加在这个虚表后面. 位置在对象的前四个字节. 对于多继承, 有多个父类, 每一个父类的继承关系都有一个虚表, 那么多继承的子类有多个虚表,分别是两个父类的虚表, 位置在每个父类的前四个字节, 子类新增特有的虚函数, 添加在第一个父类虚表中.

虚继承的性质

  1. 父类和子类在空间上会形成倒置关系.
  2. 子类特有的虚函数会形成子类特有的虚函数表, 可能一层继承关系, 就存在多个虚表(父的, 子新增的).
  3. 子类中存在虚基表, 存储指向自己和父类的指针.
  4. 最底层的子类, 完成虚基表的构造.
    虚继承解决的问题: 在继承间接共同基类时, 只保留一份成员.可以解决继承向上的二义性.
    菱形继承(普通)
    在这里插入图片描述
    这种编译都不能通过.
    多继承中的虚继承: 父类倒置
    在这里插入图片描述

虚继承的菱形继承1: 多层
在这里插入图片描述

虚继承的菱形继承2

在这里插入图片描述

指针与引用的区别

引用: 必须初始化, 不占空间, 唯一 不可改变指向 , 无空引用
指针: 32位系统指针占用4个字节, 初始化不是必须, 可以改变指向, 有空指针
int & a; 对
int &
a; 错

几种特殊函数

  1. 构造函数: 构造先于对象, 无返回值, 函数名与类名相同
  2. 内联函数: 适用于函数代码较少调用频繁的场合, 在编译期完成替换
  3. 友元函数: 共享该类对象
  4. 静态成员函数: 一个类只有一个, 同一类的对象共用.
  5. 普通函数: 非类成员

接口类和抽象类

接口类: 类内函数都是纯虚函数, 不能实例化. 有想法, 但没有实现.
抽象类: 不能被实例化, 将定义了纯虚函数的类称为抽象类, 抽象类可以包括抽象方法,这是普通类所不能的,但同时也能包括普通的方法。抽象方法只能声明于抽象类中,且不包含任何实现,派生类必须覆盖它们。
继承抽象类的子类, 必须实现父类的所有方法, 才能实例化.

this指针

this中存放当前类对象的地址,用于区分非成员与成员的变量和方法

常用的设计模式

了解常用的设计模式

STL

STl的组成简单介绍

容器 (序列容器和关联容器) 迭代器, 算法 , 空间配置器, 配接器, 仿函数
序列容器 vector deque list stack queue

vector 是动态数组, 支持快速随机访问, 涉及扩容的问题. 插入删除操作涉及向前或向后移动.

deque是双向队列, 结构如下, 两端插入删除操作, 比较方便. 支持首尾(中间不能)快速增删,也支持随机访问

list 双向链表
stack queue是deque的基础上包了一层结构.
关联容器 set map hashmap , 首先要了解映射关系, 键值对.

set: 集合, 底层数据结构为红黑树,有序,不重复。 自动排序, 键值与实值相等.

map:底层数据结构为红黑树,有序,不重复。

hash_map :底层数据结构为hash表,无序,不重复。结构就是 vector 包含list, list的每个元素是pair.

空间配置器: 负责空间分配和管理.

配接器: 为空间配置器 , 迭代器, 仿函数提供接口的结构
仿函数: 对象内部重载()方法.

容器底层

vector就是一个动态数组,里面有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另一片更大的空间(一般是增加当前容量的50% ),然后把原来的数据拷贝过去,接着释放原来的那片空间;当释放或者删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

迭代器的失效问题

序列容器, 涉及迭代器失效的问题, 对于vector
insert时,假设insert位置在p,分两种情况:
a) 容器还有空余空间,不重新分配内存,那么p之前的迭代器都有效,p之后的迭代器都失效
b) 容器重新分配了内存,那么所有的迭代器都无效
erase删除某一个结点之后,vector迭代器虽然还是指向当前位置,而且也引起了元素前挪,但是由于删除结点的迭代器就已经失效,指向删除点后面的元素的迭代器也全部失效,所以不能对当前迭代器进行任何操作;需要对迭代器重新赋值或者接收erase它的返回值;
对于list , 插入有效, erase ,所有迭代器, 引用指针都有效.
对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
也就是插入删除deque 迭代器都是失效的.

相近数据结构比较

何时选择vector ,何时选择list , 主要分为插入删除和随机访问两个方面.

红黑树有什么性质?
1)每个结点是红色或者黑色。
2)根结点为黑色。
3)叶结点为黑色的NULL结点。
4)如果结点为红,其子节点必须为黑。
5)任一结点到NULL的任何路径,所含黑结点数必须相同。

常见问题

hash_map与map的区别?什么时候用hash_map,什么时候用map?
构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。
存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。
总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
如果考虑效率,特别当元素达到一定数量级时,用hash_map。
考虑内存,或者元素数量较少时,用map。
hash_map底层是散列的所以理论上操作的平均复杂度是常数时间,map底层是红黑树,理论上平均复杂度是O(logn),这里总结一下,选用map还是hash_map,关键是看关键字查询操作次数,以及你所需要保证的是查询总体时间还是单个查询的时间。如果是要很多次操作,要求其整体效率,那么使用hash_map,平均处理时间短。如果是少数次的操作,使用 hash_map可能造成不确定的O(N),那么使用平均处理时间相对较慢、单次处理时间恒定的map,考虑整体稳定性应该要高于整体效率,因为前提在操作次数较少。如果在一次流程中,使用hash_map的少数操作产生一个最坏情况O(N),那么hash_map的优势也因此丧尽了。
总结起来, 就是考虑内存使用map , 不考虑内存, 操作次数较多, 追求效率使用hash_map , 操作次数较少用map. 而且map具有自动排序.

map和set的3个问题。
1)为何map和set的插入删除效率比其他序列容器高。
因为不需要内存拷贝和内存移动
2)为何map和set每次Insert之后,以前保存的iterator不会失效?
因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。
2)当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?
RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

特殊方法

remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SS_zico

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值