c++面经整理(二)c++面向对象

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竟然有两个虚指针
3333
每个虚表里都有一个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 赋值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值