1. 面向对象三大特性
1.1 封装(Encapsulation)
封装是将数据(属性)和操作这些数据的函数(方法)组合在一个类(Class)中的过程。
封装的主要目的是隐藏类的内部实现细节,仅暴露必要的接口给外部。
通过封装,我们可以控制类成员的访问级别(例如:public、protected 和 private),限制对类内部数据的直接访问,确保数据的完整性和安全性。
1.2 继承(Inheritance)
继承是一个类(派生类,Derived Class)从另一个类(基类,Base Class)那里获得其属性和方法的过程。
继承允许我们创建具有共享代码的类层次结构,减少重复代码,提高代码复用性和可维护性。
在 C++ 中,访问修饰符(如 public、protected、private)控制了派生类对基类成员的访问权限。
1.3 多态(Polymorphism)
1.3.1 C++多态的实现方式
C++实现多态的方法主要包括虚函数、纯虚函数和模板函数
其中虚函数、纯虚函数实现的多态叫动态多态,模板函数、重载等实现的叫静态多态
C++ 的多态必须满足两个条件:
必须通过基类的指针或者引用调用虚函数
被调用的函数是虚函数,且必须完成对基类虚函数的重写
class Person //成人
{
public:
virtual void fun()
{
cout << "全价票" << endl; //成人票全价
}
};
class Student : public Person //学生
{
public:
virtual void fun() //子类完成对父类虚函数的重写
{
cout << "半价票" << endl;//学生票半价
}
};
void BuyTicket(Person* p)
{
p->fun();
}
int main()
{
Student st;
Person p;
BuyTicket(&st);//子类对象切片过去
//BuyTicket(st);会调用基类的方法,要传地址
BuyTicket(&p);//父类对象传地址
}
1.3.2 虚函数表
class A
{
public:
virtual void fun1()
{}
virtual void fun2()
{}
};
class B : public A
{
public:
virtual void fun1()//重写父类虚函数
{}
virtual void fun3()
{}
};
A a;
B b; //我们通过调试看看对象a和b的内存模型。
虚函数存在哪里?虚函数表存在哪里?
虚函数是带有virtual的函数,虚函数表是存放虚函数地址的指针数组,虚函数表指针指向这个数组。对象中存的是虚函数指针,不是虚函数表。
虚函数和普通函数一样存在代码段。
我们创建两个A对象,发现他们的虚函数指针相同,这说明他们的虚函数表属于类,不属于对象。所以虚函数表应该存在共有区。虚函数表放在了全局数据段。
1.3.3 多态实现的原理
class Person //成人
{
public:
virtual void fun()
{
cout << "全价票" << endl; //成人票全价
}
};
class Student : public Person //学生
{
public:
virtual void fun() //子类完成对父类虚函数的重写
{
cout << "半价票" << endl;//学生票半价
}
};
void BuyTicket(Person* p)
{
p->fun();
}
满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。
普通函数的调用是编译时就确定的。
1.3.4 继承中的虚函数表
class BV
{
public:
virtual void Fun1()
{
cout << "BV->Fun1()" << endl;
}
virtual void Fun2()
{
cout << "BV->Fun2()" << endl;
}
};
class DV : public BV
{
public:
virtual void Fun1()
{
cout << "DV->Fun1()" << endl;
}
virtual void Fun3()
{
cout << "DV->Fun3()" << endl;
}
virtual void Fun4()
{
cout << "DV->Fun4()" << endl;
}
};
虚函数表:
多继承
class A
{
public:
virtual void fun1()
{
cout << "A->fun1()" << endl;
}
protected:
int _a;
};
class B
{
public:
virtual void fun1()
{
cout << "B->fun1()" << endl;
}
protected:
int _b;
};
class C : public A, public B
{
public:
virtual void fun1()
{
cout << "C->fun1()" << endl;
}
protected:
int _c;
};
C c;
//sizeof(c) 是多少呢?
sizeof( c )的大小是多少呢?是16吗?一个虚表指针,三个lnt,考虑内存对齐后确实是16.但是结果是20.
我们来看看内存模型。在VS下,c竟然有两个虚指针
每个虚表里都有一个fun1函数。
所以C的内存模型应该是这样的,
更多可以参考。
1.3.5 为什么 C++ 构造函数不能是虚函数?
1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数。
1.3.6 为什么 C++ 基类析构函数需要是虚函数?
首先析构函数可以为虚函数,当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全造成内存泄漏。
2. 重载、重写、隐藏的区别
2.1 重载(Overloading):
重载是指相同作用域(比如命名空间或者同一个类)内拥有相同的方法名,但具有不同的参数类型和/或参数数量的方法。 重载允许根据所提供的参数不同来调用不同的函数。它主要在以下情况下使用:
方法具有相同的名称。
方法具有不同的参数类型或参数数量。
返回类型可以相同或不同。
同一作用域,比如都是一个类的成员函数,或者都是全局函数。
class OverloadingExample {
void display(int a) {
System.out.println("Display method with integer: " + a);
}
void display(String b) {
System.out.println("Display method with string: " + b);
}
}
2.2 重写(Overriding)
重写是指在派生类中重新定义基类中的方法。
当派生类需要改变或扩展基类方法的功能时,就需要用到重写。
重写的条件包括:
方法具有相同的名称。
方法具有相同的参数类型和数量。
方法具有相同的返回类型。
重写的基类中被重写的函数必须有virtual修饰。
重写主要在继承关系的类之间发生。
#include <iostream>
class BaseClass {
public:
virtual void display() {
std::cout << "Display method in base class" << std::endl;
}
};
class DerivedClass : public BaseClass {
public:
void display() override {
std::cout << "Display method in derived class" << std::endl;
}
};
int main() {
DerivedClass derived;
derived.display();
return 0;
}
2.3 隐藏(Hiding)
隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏
#include<iostream>
using namespace std;
classA{
public:
void fun1(int i, int j){
cout <<"A::fun1() : " << i <<" " << j << endl;
}
};
classB : public A{
public:
//隐藏
void fun1(double i){
cout <<"B::fun1() : " << i << endl;
}
};
int main(){
B b;
b.fun1(5);//调用B类中的函数
b.fun1(1, 2);//出错,因为基类函数被隐藏
system("pause");
return 0;
}
2.4 总结
重载和重写的区别:
范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中(同一作用域)。
参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
隐藏和重写,重载的区别:
与重载范围不同:隐藏函数和被隐藏函数在不同类中。
参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。
class A{
public:
void fun1(int i, int j){
cout <<"A::fun1() : " << i <<" " << j << endl;
}
void fun2(int i){
cout <<"A::fun2() : " << i << endl;
}
virtual void fun3(int i){
cout <<"A::fun3(int) : " << i << endl;
}
};
class B : public A{
public:
//隐藏
void fun1(double i){
cout <<"B::fun1() : " << i << endl;
}
//重写
void fun3(int i){
cout <<"B::fun3(int) : " << i << endl;
}
//隐藏
void fun3(double i){
cout <<"B::fun3(double) : " << i << endl;
}
};
3. C++ 类对象的初始化和析构顺序
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Base1 {
public:
Base1() { std::cout << "Base1 constructor" << std::endl; }
~Base1() {
std::cout << "Base1 destructor" << std::endl;
}
};
class Base2 {
public:
Base2() { std::cout << "Base2 constructor" << std::endl; }
~Base2() {
std::cout << "Base2 destructor" << std::endl;
}
};
class Base3 {
public:
Base3() { std::cout << "Base3 constructor" << std::endl; }
~Base3() {
std::cout << "Base3 destructor" << std::endl;
}
};
class MyClass : public virtual Base3, public Base1, public virtual Base2 {
public:
MyClass() : num1(1), num2(2) {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
private:
int num1;
int num2;
// 这个是为了看成员变量的初始化顺序
Base base;
};
int main() {
MyClass obj;
return 0;
}
输出结果:
Base3 constructor // 虚继承排第一
Base2 constructor // 虚继承优先
Base1 constructor // 普通基类
Base constructor // 成员函数构造
MyClass constructor // 构造函数MyClass destructor//析构完全相反
Base destructor
Base1 destructor
Base2 destructor
Base3 destructor
4. 深拷贝和浅拷贝
4.1 浅拷贝
浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。( 浅拷贝是一种简单的拷贝方式,它仅复制对象的基本类型成员和指针成员的值,而不复制指针所指向的内存)。
这可能导致两个对象共享相同的资源,从而引发潜在的问题,如内存泄漏、意外修改共享资源等。
一般来说编译器默认帮我们实现的拷贝构造函数就是一种浅拷贝。
4.2 深拷贝
深拷贝不仅复制对象的基本类型成员和指针成员的值,还复制指针所指向的内存。
因此,两个对象不会共享相同的资源,避免了潜在问题。
深拷贝通常需要显式实现拷贝构造函数和赋值运算符重载。
#include <iostream>
#include <cstring>
class MyClass {
public:
MyClass(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 深拷贝的拷贝构造函数
MyClass(const MyClass& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 深拷贝的赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this == &other) {
return *this;
}
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
return *this;
}
void SetString(const char* str) {
if (data != NULL) {
delete[] data;
}
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~MyClass() {
delete[] data;
}
void print() {
std::cout << data << std::endl;
}
private:
char* data;
};
int main() {
MyClass obj1("Hello, World!");
MyClass obj2 = obj1; // 深拷贝
obj1.print(); // 输出:Hello, World!
obj2.print(); // 输出:Hello, World!
// 修改obj2中的数据,不会影响obj1
obj1.SetString("Test");
obj2.print(); // 输出:Hello, World!
return 0;
}
4.3 总结
浅拷贝和深拷贝都即可以在栈上进行也可以在堆山进行,此外浅拷贝只会复制指针的值,而不会复制这个指针指向的数据;深拷贝会复制这个指针指向的数据(深拷贝是新开辟一块空间,将原对象的资源复制到新的空间中,并返回该空间的地址 )。深拷贝可以避免重复释放和写冲突。例如使用浅拷贝的对象进行释放后,对原对象的释放会导致内存泄漏或程序崩溃。
5. this 指针
5.1. this 指针
this 是一个指向当前对象的指针。
其实在面向对象的编程语言中,都存在this指针这种机制, Java、C++ 叫 this,而 Python 中叫 self。
在类的成员函数中访问类的成员变量或调用成员函数时,编译器会隐式地将当前对象的地址作为 this 指针传递给成员函数。
因此,this 指针可以用来访问类的成员变量和成员函数,以及在成员函数中引用当前对象。
5.2 static 函数不能访问成员变量
在C++中,static 函数是一种静态成员函数,它与类本身相关,而不是与类的对象相关。
大家可以将 static 函数视为在类作用域下的全局函数,而非成员函数。
因为静态函数没有 this 指针,所以它不能访问任何非静态成员变量。
如果在静态函数中尝试访问非静态成员变量,编译器会报错。
下面这个示例代码,说明了静态函数不能访问非静态成员变量:
class MyClass {
public:
static void myStaticFunction() {
// 以下代码会导致编译错误
// cout << this->myMemberVariable << endl;
}
private:
int myMemberVariable;
};
在上面的示例中,myStaticFunction是一个静态函数,尝试使用this指针来访问非静态成员变量myMemberVariable,但这会导致编译错误。
5.3 总结
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。
不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。