C++面向对象篇个人总结,需要自己动手验证。
类如何实现在外部只能静态分配或动态分配:私有析构;私有delele
class A {//私有析构函数就无法在栈上分配
private:
~A();
};
class A {//私有delete就无法在堆上分配
private:
void operator delete(void* p);
}
//私有构造函数:则堆栈都无法分配
单例模式:构造函数私有化(则静态动态都不能外部分配了),禁用拷贝构造、赋值函数,提供公有静态接口,返回静态局部对象
class Singleton {
public:
// 删除拷贝构造函数和赋值操作符,确保单例不能被复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 提供全局访问点
static Singleton& getInstance() {
static Singleton instance; // C++11保证静态局部变量的线程安全性
return instance;
}
private:
// 私有构造函数,防止外部创建实例
Singleton() = default;
~Singleton() = default;
};
注意:单例可以引用:Singleton& instance = Singleton::getInstance();
工厂模式:把对象的创建与使用分离,客户端只需要调工厂的接口获得对象,而不用管它是如何生产的。
#include <memory>
#include <iostream>
//产品抽象类
class Product {
public:
virtual void use() = 0;
virtual ~Product() {}
};
class ProductA: public Product {
public:
ProductA() {}
~ProductA() {}
void use() { std::cout << "ProductA" << std::endl; }
};
class ProductB: public Product {
public:
ProductB() {}
~ProductB() {}
void use() { std::cout << "ProductB" << std::endl; }
};
//工厂类,生产产品
class Factory {
public:
enum Type {
A,
B
};
static std::unique_ptr<Product> getProduct(Type t) {
switch (t) {
case Type::A:
return std::make_unique<ProductA>();
case Type::B:
return std::make_unique<ProductB>();
default:
return nullptr;
}
}
};
int main() {
auto p = Factory::getProduct(Factory::Type::A);
if (p) {
p->use();
}
else {
std::cout << "输入无效!" << std::endl;
}
}
友元:允许类外函数访问本类的所有成员,就是最好的朋友。但注意友元不能被继承(你爸的朋友不是你的朋友),友元是单向的,且不具有传递性。
友元分友元函数,友元类;在谁里面声明就是告诉别人他是我好朋友,它可以访问我。
当然声明定义可以写一起,如用于函数重载实现对象相加访问对象私有成员。
class B{
private:
int a = 1;
friend class A;//A可以访问我
};
class A {
public:
void func() {
B b;
cout << b.a;
}
};
重载overload、重写override、隐藏overwrite:重载:一组同名函数,形参不同;
重写:子类重写父类的虚函数函数实现多态,且重写的函数要与父类形式保持一致。隐藏:子类屏蔽了父类的同名非虚函数,形参返回值可以不同,当子类对象调用此函数,调的就是子类的,父类的被隐藏了。
class dad{
public:
void func() {
cout << 1<< endl;
}
};
class son: public dad{
public:
void func(int a) {//这个函数就把父类的func()隐藏了,想用必须显式调用
cout << 2;
}
};
所有的构造函数:无参构造(默认构造),有参构造(可以重载多个);拷贝构造:用对象构造初始化对象(分显式拷贝,隐式拷贝)、以值传递传对象做参数、函数以值传递返回局部对象都会调用;移动构造,接收右值引用转移右值的资源。
为什么拷⻉构造函数必须是引⽤传递,不能是值传递:防止递归调用。若是值传递,值传递本身就需要调用拷贝构造函数,无限递归。
类成员初始化方式:在构造函数里等号赋值初始化,成员初始化列表(更快,一个是初始化,一个是赋值),因为构造函数内部赋值需要拷贝创建临时副本,再赋值,成员初始化列表直接是初始化赋初值;且有些情况必须成员初始化列表:成员是常量或引用(因为他俩必须在创建时初始化,不能说拷贝赋值)、若基类、成员类对象(成员有其他类的对象)无默认构造也必须使用列表显式调用他们的有参构造。调用过程:编译器会把初始化列表中的成员按声明顺序在构造函数内部进行初始化操作。
class A{
public:
A(int m) {}
};
class dad{
public:
dad(int n) {}
};
class son: public dad{
public:
const int a;
int& b;
A sa;
son(int m, int n, int m_b): sa(1), b(m_b),dad(n), a(m) {}
};
int main() {
son s(1,2,3);
}
子类构造函数的执行顺序:虚基类(菱形继承中的A)-基类-成员类对象-派生类;析构相反
构造函数的扩展过程:成员初始化列表的成员放入构造函数内部、虚表指针的创建、虚基类,基类和成员类对象的构造得调用。
构造函数析构函数可否抛出异常:可以,但最好不要:构造函数抛出异常会导致对象创建失败,而析构函数只会析构对象即不会调用,造成内存泄露;析构函数抛出异常可能导致资源没有释放。真要用的话内部就得捕捉处理。
什么情况下编译器会自动生成(合成)默认构造函数/拷贝构造函数:当没有定义任何构造且需要默认构造干活时,编译器才会自动生成。如1.没定义任何构造函数,创建对象,需要默认构造完成,所以会生成;2.当你是派生类/你的成员有其他类对象,创建子类对象时需要先调用基类的和成员类对象的默认构造,这调用的工作默认构造来完成,所以会生成。
空类有哪些默认函数:默认构造,析构,拷贝构造(用一个对象构造另一个对象),拷贝赋值运算符(把一个对象值赋给另一个对象,返回引用),移动构造,移动赋值运算符。但注意:只有需要他们的时候才会生成。
计算类大小:空类内存1;非空类=虚函数表指针+非静态成员+内存对齐,这就引出c++对象的内存模型:先:虚表指针(在最前面)->非静态成员变量,静态数据在全局区,函数都在代码区。有的编译器会把虚基表指针和虚表指针合并。
举例:虚表指针在最前面,全部向他对齐。
//sizeof(Fat()) = 16;
class Fat {
public:
virtual void fun() {}
char b;
char a;
};
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base构造函数" << std::endl;
virtualFunction(); // 这里会调用Base类中的virtualFunction
}
virtual void virtualFunction() {
std::cout << "Base的虚函数" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived构造函数" << std::endl;
}
void virtualFunction() override {
std::cout << "Derived的虚函数" << std::endl;
}
};
int main() {
//先调父类构造,父类构造里再调虚函数,但此时子类尚未完成构造,即无虚表指针,所以不发生多态,调父类自己的虚函数。
Derived* d = new Derived();
delete d;
return 0;
}
封装:把数据和方法封装成一个抽象的类,对外部提供接口,隐藏了内部细节,实现面向对象编程。
继承:
1、定义:一个类继承另一个类的属性和方法。通过继承子类获得父类的特性,还拥有自己的特性,好处是代码重用易扩展。支持单继承,多继承(一个人继承多个),虚拟继承:因为会出现菱形继承问题:d中出现2次a的成员,浪费内存,且有2义性:到底用从B继承的a还是C及继承的a。为了解决,引入虚拟继承,class B:public virtual A;则虚基类A无论被继承多少次,只会存在一份副本。
2、虚拟继承的原理:虚继承的子类BC会各自产生一个虚基类指针指向虚基类表,虚基类表记录了虚基类指针到从A类成员的地址偏移量。D类继承BC也会生成2个虚基类指针和2个虚基类表。现在我想访问A类成员,我还是有2条路走:分别通过2个虚基表访问到2个虚基表,但是你得到的最终地址是一样的,如:虚基类指针1位于0位置,虚基类表记录40,指针2位于16,虚基类表记录24,最终都指向D类对象内存40 的位置,这里存放了a的内容。这样就不需要拷贝2份,也没有二义性。
3、B的内容(虚基类指针+B类自己成员) + C内容+ D内容+A成员(最后访问的都是它)。注意:虚继承与虚函数没有关系,虚函数表,虚基类表,虚函数表指针,虚基类表指针是不同的东西,可共存,但某些编译器会合并。
4、继承也会继承父类的静态成员,即子类和父类所有对象共享一个静态成员变量。
组合:一个类把另一个类对象作为自己的成员。继承优点:代码复用易扩展,缺点高耦合,子类很依赖父类。组合:低耦合,易实现代码分层,缺点:组合太多会变得很复杂。
使用场景:当基类是抽象类时适合继承如人与婴儿,其他情况组合更好,如人为了使用鸟的飞行继承就不好,组合更好。
public,protected,private:3种访问权限类内都能访问,类外只能访问公有成员。3种继承方式:首先父类的私有成员无论什么继承方式,子类都不可见(但确实继承了,占内存),下面只需讨论公有成员和保护成员在3种继承方式下的情况:基类的公有成员和保护成员被子类公有继承后访问权限不变,保护继承后变成保护成员,被私有继承后变成私有成员。
个人理解:给外部提供的接口就设为public,给内部使用的函数就设为private,继承再考虑protected。
多态:一个接口接收不同对象,产生不同行为,好处是提供一个统一的接口,代码易维护。多态实现:静态多态就是函数重载,模板,编译时就确定了调用类型;动态多态:父类指针或引用指向子类对象,子类重写父类虚函数,当基类指针/引用调用虚函数时会根据对象类型选择调用哪个子类的方法。动态多态实现原理:首先在基类函数加virtual,就成了虚函数。当类中有虚函数时,编译期会在常量区生成一个虚函数表(一维数组,存放类中所有虚函数地址),运行时调用构造函数会创建一个虚表指针(位于对象地址首位),指向虚表;单继承举例:子类继承有虚函数的父类,自然也生成自己的虚表指针和虚表,虚表是拷贝父类的,再把重写的虚函数地址改掉,最后放入子类自己的虚函数地址(若有)。运行时:当父类指针或引用指向子类对象,拿到子类的虚表指针,查虚表调用虚函数。
**多继承:**上面的多态说的是单继承下的,若是子类继承2个虚类A,B,那会产生2个虚表指针和2张虚函数表,而子类自己的虚函数会存到第一个父类的虚表里。
基类析构函数要写成虚函数:若不是虚函数,就没有多态,当delete父类指针时只会执行父类的析构,不会释放子类对象,造成内存泄露。定义成虚函数,就会根据对象类型调用子类的析构函数而子类的析构又会自动调用父类的析构。
#include <iostream>
class Base {
public:
virtual ~Base() {std::cout << "Base destructor called" << std::endl;}
};
class Derived : public Base {
public:
~Derived() {std::cout << "Derived destructor called" << std::endl;}
};
int main() {
Base* obj = new Derived();
delete obj;
}
多态的个人体会:就是抽象一个类出来,后续大家都继承它,复用代码和接口,如monitorpro模块。
构造函数为什么⼀般不定义为虚函数:若构造函数是虚的,想调用它必须通过虚函数表指针,可是虚函数表指针就是在构造函数里创建的,即构造函数调用前虚函数表指针没有。
内联成员函数也不能是虚函数:内联在编译时替换,虚函数在运行时才确定调用哪个虚函数,你知道替换哪个代码吗。
静态成员函数:无this指针,也就访问不了虚表指针
友元函数,普通函数:无法被继承,也就没有虚函数的说法了
在构造/析构函数内部调用虚函数:不要做。举例:基类构造函数中定义了虚函数,子类重写了,当创建子类对象时,先调用基类构造,调用虚函数,根据实际子类对象来选择虚函数调用,可此时子类对象都没被构造出来,虚函数也就没意义了。析构函数中同理:先析构子类,子类呈现未定义状态,在基类析构调用虚函数也无法根据类型选择。
纯虚函数:虚函数=0;该类就成为了抽象类,本身不能实例化,只作为一个接口,起到规范的作用。一般父类都定义纯虚,子类必须重写,不然也是抽象类。抽象基类还有个作用:用于形参,接受任意类型的子类。