• C++问答2 三大特性


    T:what
    Y: why
    W: how

    1 继承

    T

    让某种类型对象获得另一个类型对象的属性和方法

    Y

    可以使用现有类的功能,并在无需重新编写原来类的情况下对这些功能进行扩展

    W

    继承的三重方式

    实现:基类的方法和属性的使用

    接口:仅仅使用基类的方法和属性的名称,子类需要提供代码的实现

    可视:子窗口(类)使用基窗口(类)外观和代码实现

    2 封装

    T

    将数据和代码捆绑在一起。把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信的进⾏信息隐藏

    Y

    避免外界干扰和不确定访问;

    3 多态

    T

    同一背景下表现出不同的事物的能力,也就是向不同对象发送同一消息,不同的对象在接受时产生不同的行为。

    Y

    允许将子类类型的指针赋值给父类类型的指针。

    实现多态有两种方式,重载实现编译时的多态,虚函数(重写)实现运行时的多态

    W

    静态多态、动态多态、多态的实现原理、虚函数、虚函数表

    1. 静态多态 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
    2. 动态多态 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。
    3. 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。 2. 实现动态多态的条件:
    • 要有继承关系 - 要有虚函数重写(被 virtual 声明的函数叫虚函数) -
    • 要有父类指针(父类引用)指向子类对象

    动态多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。

    4 虚函数

    virtual,

    T

    用virtual修饰的成员函数,虚函数依赖保存虚函数地址的虚函数表工作,当用基类指针指向派生类时,虚表指针指向派生类的虚函数表。

    Y

    不同继承关系的类对象,调用同一函数产生不同的行为,也就是为了实现动态多态,父类想要子类定义适合自己的方法。

    W

    virtual typeReturn function()
    {
    
    }
    
    • 普通函数(非类成员函数)不能是虚函数
    • 静态函数(static)不能是虚函数,原因在于static成员没有this指针,而虚函数依赖对象调用,用隐藏的this指针。
    • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针
    • 析构函数可以是虚函数,这在一个复杂类结构往往是必须的,这是为了在析构时先调用。
    • 内联成员函数只有在不表现多态的时候才能用virtual
    #include   
    using namespace std;
    class Base
    {
    public:
        inline virtual void who()
        {
            cout << "I am Base\n";
        }
        virtual ~Base() {}
    };
    class Derived : public Base
    {
    public:
        inline void who()  // 不写inline时隐式内联
        {
            cout << "I am Derived\n";
        }
    };
    
    int main()
    {
        // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
        Base b;
        b.who();
    
        // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
        Base *ptr = new Derived();
        ptr->who();
    
        // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
        delete ptr;
        ptr = nullptr;
    
        system("pause");
        return 0;
    } 
    

    4.1 为什么构造函数不可以是虚函数

    虚函数表,对象内存空间,指针引用实现;创建对象时自动调用

    1.从存储角度

    虚函数必须要有个虚函数表(vtable),而它又存储在对象的内存空间,而对象有需要通过类来实例化构造,而这就需要构造函数,所以构造函数不能够是虚函数,不然就没有vtable

    2.从使用角度

    虚函数的作用是作为调用子类成员函数重载的一个媒介,而这个媒介就是通过父类的指针或者引用来实现的.而构造函数是在创建对象时自动调用,不可能通过父类的指针或者引用去调用

    4.2 为什么析构函数可以是虚函数

    首先说明一点,一般虚函数在前面会添加virtual修饰符,但其实不管加不加,都在所有的派生类中称为虚函数.

    编译器总是根据类型来调用类成员函数。一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏

    C++不把虚析构函数直接作为默认值的原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象总是在开始的位置包含一个隐含的虚函数表指针成员。

    #include 
    using namespace std;
     
    class Box
    {
    public:
      const char *name;
      int age;
      float score;
      Box() {
        cout << "调用构造函数Box!" << endl;
      }
      ~Box() {
        cout << "调用析构函数Box!" << endl;
      }
      void say() {
        cout << name << "的年龄是" << age << ",成绩是" << score << endl;
      }
    };
     
    class BigBox : public Box
    {
    public:
      const char *name;
      int age;
      float score;
      BigBox() {
        cout << "调用构造函数BigBox!" << endl;
      }
      ~BigBox() {
        cout << "调用析构函数BigBox!" << endl;
      }
      void say() {
        cout << name << "的年龄是" << age << ",成绩是" << score << endl;
      }
    };
     
    int main()
    {
      // new运算符,在堆上新建对象,调用构造函数,并返回该对象的指针
      Box* box = new BigBox;
      box->name = "ss";
      box->age = 18;
      box->score = 100;
      box->say();
      // delete运算符,释放堆上的对象,调用对象的析构函数
      delete box;
     
      getchar();
      return 0;
    }
    
    

    在这里插入图片描述

    如果将基类设为虚函数,那么

    #include 
    using namespace std;
     
    class Box
    {
    public:
      const char *name;
      int age;
      float score;
      Box() {
        cout << "调用构造函数Box!" << endl;
      }
      virtual ~Box() {
        cout << "调用析构函数Box!" << endl;
      }
      void say() {
        cout << name << "的年龄是" << age << ",成绩是" << score << endl;
      }
    };
     
    class BigBox : public Box
    {
    public:
      const char *name;
      int age;
      float score;
      BigBox() {
        cout << "调用构造函数BigBox!" << endl;
      }
      ~BigBox() {
        cout << "调用析构函数BigBox!" << endl;
      }
      void say() {
        cout << name << "的年龄是" << age << ",成绩是" << score << endl;
      }
    };
     
    int main()
    {
      // new运算符,在堆上新建对象,调用构造函数,并返回该对象的指针
      Box* box = new BigBox;
      box->name = "ss";
      box->age = 18;
      box->score = 100;
      box->say();
      // delete运算符,释放堆上的对象,调用对象的析构函数
      delete box;
     
      getchar();
      return 0;
    }
    
    

    在这里插入图片描述

    reference

    为什么C++的构造函数不可以是虚函数,而析构函数可以是虚函数 - 知乎 (zhihu.com)

    4.3 虚函数与纯虚函数的区别

    是否实现,声明,抽象类

    虚函数纯虚函数
    类中有无定义无(只是一个接口)
    声明virtual type function(){}virtual type function()=0;
    子类中是否实现可不重载必须实现
    类被声明为抽象类
    能直接生成对象不能直接生成对象,只有被继承
    #include
    using namespace std;
    
    class p{
        public:
            virtual void s()=0;
    };
    class c:public p{
        public:
            void s()
            {
                cout<<"gogo"<<endl;
            }
    };
    
    int main()
    {
        // cout<<"hello"<
        // p u;//error
        p *b;
        c object;
        b=&object;
        b->s();
        object.s();
    
        return 0;
    }
    
    
    class Cperson
    {
    protected:
        float mark;
        string name;
    public:
        Cperson(string name, float mark)
        {
            this->name = name;
            this->mark = mark;
        }
        ~Cperson()
        {
            cout << "调用Cperson的析构函数" << endl;
        }
        virtual void ShowInf() = 0; // 纯虚函数声明形式
        // 纯虚函数未定义函数体无法分配存储空间,因此无法定义基类对象
    };
    
    class Cstudent:public Cperson
    {
    private:
        char sex;
    public:
        Cstudent(char sex, string name, float mark) :Cperson(name, mark)
        {
            this->sex = sex;
        }
        ~Cstudent()
        {
            cout << "调用Cstudent析构函数" << endl;
        }
        void ShowInf()
        {
            cout << this->name << "性别:" << this->sex << ";分数为" << this->mark << "分" << endl;
        }
    };
    
    // 使用基类指针调用派生类对象
    void ShowInf(Cperson *person)
    {
        person->ShowInf();
    }
    
    void testsub(){
    //    A* ma=new A();//error
        Cstudent stud('m',"gogo",99);
        stud.ShowInf();
        ShowInf(&stud);
    
    }
    

    gogo性别:m;分数为99分
    gogo性别:m;分数为99分
    调用Cstudent析构函数
    调用Cperson的析构函数

    一个空类的大小?为什么?

    1,实例化

    大小由编译器决定,在vs中为1个字节,因为必须要在内存中占有一定空间,才能声明该类的实例,否则实例无法使用。

    • 如果类中添加构造函数和析构函数呢?

    同样为1,调用析构函数和构造函数只需要知道函数的地址即可,而这个地址由于类的地址关联,而与类的实例无关

    • 如果析构函数是虚函数呢?

    编译器会为类生成虚函数表,并为该类的每个实例添加一个指向虚函数表的指针,32位机器一个指针占4个字节,64位8个字节

    4.5 虚继承

    T

    在继承方式后添加virtual关键字的继承

    Y

    • 为了解决多继承命名冲突和数据冗余(rong)问题。
    • 为了让某个类做出声明,表示愿意共享它的基类。
    #include"../main.hpp"
    using namespace std;
    //间接基类A
    class A{
    protected:
        int m_a;
    };
    
    //直接基类B
    class B: virtual public A{  //虚继承
    protected:
        int m_b;
    };
    
    //直接基类C
    class C: virtual public A{  //虚继承
    protected:
        int m_c;
    };
    
    //派生类D
    class D: public B, public C{
    public:
        void seta(int a){ m_a = a; }  //正确
        //如果不使用虚继承,那么就会导致命名冲突
        //需要指定作用域,如B::m_a=a,C::m_a=a;
        void setb(int b){ m_b = b; }  //正确
        void setc(int c){ m_c = c; }  //正确
        void setd(int d){ m_d = d; }  //正确
    private:
        int m_d;
    };
    
    int main(){
        D d;
        return 0;
    }
    
    

    C++虚继承和虚基类详解 (biancheng.net)

    在这里插入图片描述

    5 C++类与结构体的区别

    关键字不同,都能够继承

    结构体
    默认权限·私有公有
    能否作为模板关键字
    内存分配方面引用类型值类型
    继承关系上私有公有
    template<typename T, typename Y>    // 可以把typename 换成 class 
    int Func(const T& t, const Y& y) { 
        //TODO 
    }
    

    在C语言中结构体不能为空,并且只涉及到数据结构,不能够定义方法

    值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中。

    值类型(value type):byte,short,int,long,float,double,char,bool 和 struct 统称为值类型。值类型变量声明后,不管是否已经赋值,编译器为其分配内存。值类型的实例常在线程栈上分配(静态),但是某些情况下可以存在堆中。在内存管理方面具有更好的效率,但不支持多态,适合用做存储数据的载体。将一个值类型变量赋给另一个值类型变量时,将复制包含的值。

    引用类型(reference type):string 和 class统称为引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。引用类型的对象总在进程堆中分配(动态),支持多态,适合用于定义应用程序的行为。

    引用类型变量的赋值只复制对对象的引用,而不复制对象本身

    (182条消息) 值类型和引用类型的区别,struct和class的区别_Christal_RJ的博客-CSDN博客_struct是值类型还是引用类型

    6 简述拷贝赋值和移动赋值

    lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

    T

    拷贝赋值是通过拷贝构造函数,移动赋值通过移动构造函数赋值

    二者最大的区别就是是否能够修改被拷贝对象,因为能够取地址的对象都是左值,不能够的是右值

    拷贝赋值移动赋值
    形参左值引用右值引用
    整个对象或变量的拷贝生成一个指针指向源对象或变量的地址,接管源对象的内存
    效率高(节省时间和内存)
    #include "../main.hpp"
    using namespace std;
    
    class Person{
    // 使用一个已经创建完毕的对象来初始化一个新对象
    //值传递的方式给函数参数传值
    //以值方式返回局部对象classPerson{
    public:
        int mAge;
        Person(){
            cout<<"person default constructor"<<endl;
    
        }
        Person(int age){//有参构造
            mAge=age;
        }
        Person(const Person& p)//拷贝构造
        {
            cout<<"copy constructor"<<endl;
            mAge=p.mAge;
        }
        Person& operator=(const Person& person)
        {
            cout<<" value constructor "<<endl;
            return *this;
        }
        Person(Person &&p)//移动构造
        {
            p.mAge=6;
            cout<<"move constructor"<<endl;
        }
        //destructor
        ~Person(){
            cout<<"destructor"<<endl;
        }
    
    };
    //call
    void object(){
            Person man(100); //p对象已经创建完毕Person newman(man); //调用拷贝构造函数
            Person newman2 = man; //拷贝构造
            cout<<"newman2.mAge "<<newman2.mAge<<endl;
            //Person newman3;//newman3 = man; //不是调用拷贝构造函数,赋值操作
    
    }
    //2. 值传递的方式给函数参数传值//相当于Person p1 = p;
    void doWork(Person p1){
        p1.mAge=100;//不会影响调用函数的数据cout<<"p1.mAge:"<
    }
    
    void valueTransfer(){
        Person p;
        doWork(p);//这个p与doWork中的P不一样
        cout<<"p.mAge:"<<p.mAge<<endl;
    }
    //3. 以值方式返回局部对象
    Person doWork2(){
        Person p1;
        cout<<(int *)&p1<<endl;
        return p1;/*拷贝一个新的对象给外面*/
    }
    
    void returnValue(){
        Person p=doWork2();
        cout<<(int *)&p<<endl;
    }
    Person* createPerson()
    {
        Person* pPerson=new Person();
        return pPerson;
    }
    
    int main(){
        Person* person=createPerson();
        delete person;
        Person person1;
        Person person2=move(person1);
        cout<<person1.mAge<<endl;//error  源对象内存已被接管!!!
        // object();
        //valueTransfer();//returnValue();
        return 0;
    }
    
    

    输出:

    person default constructor
    destructor
    person default constructor
    move constructor
    6
    destructor
    destructor
    

    C++11 move()函数:将左值强制转换为右值 (biancheng.net)

    (182条消息) c++: 移动构造/赋值 和 拷贝构造/赋值_EverNoob的博客-CSDN博客_移动赋值和拷贝赋值

    7 delete this 合法吗

    new创建, 最后一个调用this,

    看情况:

    如果this对象是通过new创建的(不是new[],不在栈上、或者其他成员对象)

    delete this的成员函数是最后一个调用this的成员函数

    必须保证成员函数的delete this后面没有调用this了

    必须保证delete this 后没有人使用了

    8 右值引用其实是一个左值

    matrix k=a+b+c;

    每次加法都会调用拷贝构造函数,消耗性能

    这时需要移动构造函数;

    class Matrix{
    
    public:
        int row,col;
        float** data=nullptr;
        Matrix(int _row,int _col):row(_row),col(_col)
        {
            cout<<"call Matrix constructor"<<endl;
    
            data=new float* [row];
            for(int i=0;i<row;i++)
            {
                data[i]=new float[col];
                for(int j=0;j<col;j++)
                    data[i][j]=0;
            }
    
        }
        Matrix(const Matrix& mat)
        {
            cout<<"call Matrix copy"<<endl;
            row=mat.row;
            col=mat.col;
            data= new float*[row];
            for(int i=0;i<row;i++)
            {
                data[i]=new float[col];
                for(int j=0;j<col;j++)
                {
                    data[i][j]=mat.data[i][j];
                }
            }
        }
        Matrix(Matrix && mat)//当函数试图返回一个对象时,会调用移动构造函数,若无则调用拷贝构造函数
        {
            cout<<"call Matrix move"<<endl;
            row=mat.row;
            col=mat.col;
            data=mat.data;//无需重新分配内存
            mat.data=nullptr;//note
        }
    
        ~Matrix(){
            cout<<"call ~Matrix"<<endl;
            if(data!=nullptr)
            {
                for(int i=0;i<row;i++)
                {
                    if(data[i]){
                         delete[] data[i];
                         data[i]=nullptr;
                    }
                   
                }
                delete[] data;
                data=nullptr;
            }
        }
        // Matrix operator+(const Matrix& mat)
        // {
        //     if(mat.row!=row||mat.col!=col)
        //     {
        //         cout<<"error"<
        //     }
        //     Matrix res(mat.row,mat.col);
        //     for(int i=0;i
        //     {
        //         for(int j=0;j
        //         {
        //             res.data[i][j]=data[i][j]+mat.data[i][j];
        //         }
        //     }
        //     return res;
        // }
        //运算符有多个参数
         friend Matrix operator+(const Matrix& a,const Matrix& b)//Matrix operator+(const Matrix& mat)&&
        {
            cout<<"friend Matrix operator+(const Matrix& a,const Matrix & b)"<<endl;
            if(a.row!=b.row||a.col!=b.col)
            {
                cout<<"error"<<endl;
            }
            Matrix res(a.row,a.col);
            for(int i=0;i<a.row;i++)
            {
                for(int j=0;j<a.col;j++)
                {
                    res.data[i][j]=b.data[i][j]+a.data[i][j];
                }
            }
            return res;
        }
        friend Matrix operator+( Matrix&& a,const Matrix& b)//也可以用Matrix operator+(const Matrix& mat)&&
        {
            cout<<"friend Matrix operator+( Matrix&& a,const Matrix & b)"<<endl;
            if(a.row!=b.row||a.col!=b.col)
            {
                cout<<"error"<<endl;
            }
            // Matrix res(a.row,a.col);
            // Matrix res=a;//Lvalue reference, 调用拷贝构造函数
            //但是这样效率降低
            Matrix res=std::move(a);//a的内容已经移动,后面不能够使用a.data[i][j];
            for(int i=0;i<a.row;i++)
            {
                for(int j=0;j<a.col;j++)
                {
                    res.data[i][j]=b.data[i][j]+res.data[i][j];
                }
            }
            return res;
        }    
    };
    void testMatrix()
    {
        Matrix a(3,4),b(3,4),c(3,4),d(3,4);
        Matrix r=a+b+c+d;
    
        //右值引用是一个左值
    }
    
    call Matrix constructor
    call Matrix constructor
    call Matrix constructor
    call Matrix constructor
    friend Matrix operator+(const Matrix& a,const Matrix & b)
    call Matrix constructor
    friend Matrix operator+( Matrix&& a,const Matrix & b)
    call Matrix move //第二次调用加法便采用移动构造,右值引用
    friend Matrix operator+( Matrix&& a,const Matrix & b)
    call Matrix move
    call ~Matrix
    call ~Matrix
    call ~Matrix
    call ~Matrix
    call ~Matrix
    call ~Matrix
    call ~Matrix
    

    【乔红】裤衩 C++ 之 右值引用(二)右值引用详解_哔哩哔哩_bilibili

    9 重载与动态多态的区别

    重载多态
    1.绑定方法静态绑定,也称早绑定,在方法调用之前,编译器就确定了要调用的方法动态绑定,也称晚绑定,只有方法要调用的时候,编译器才会确定
    2.参数同名,参数不同同名同参数,需要通过函数的实际类型来确定要调用那个函数。

    10 函数重载 const修饰参数

    https://www.nowcoder.com/discuss/1025870?type=2&channel=-1&source_id=discuss_terminal_discuss_hot_nctrack

    如果参数类型使用const进行修饰,那么函数是否能重载成功呢?

    当使用const修饰函数参数时,函数重载是否生效取决于是顶层const还是底层const,简单来说就是如果函数参数是顶层const,即对于编译器来说无法区分参数本身是否是常量,所以当参数是否是常量时,无法进行重载。但是当const修饰的是某种类型的引用或者指针时,那么就可以实现函数重载。

    11 函数重载如果不在同一作用域,重载是否还会生效

    重载不会生效,因为编译器在当前作用域下找不到对应参数的同名函数,所以造成重载失败。

    12 C语言中有函数重载吗?

    没有,编译时直接根据函数名来确定连接符号,而C++则需要参数的类型和个数

    13 如何在C++项目中编译C语言代码呢?

    将c函数的实现括在 extern "C"中
    有没有办法让一段C函数代码既能在C编译器中编译,又能在C++编译器中编译。

    使用 __cpluscplus这个宏名,当使用C++编译器时,将会将中间的代码作为C语言进行编译,而如果使用C语言编译器,编译器中没有对应的宏定义,所以代码上下的宏定义都是无效的,呈现给编译器的就是一段C语言代码。

    #ifdef __cplusplus
    extern "C"//告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
    {
    #endif
        void printfs(){
            printf("gogo\n");
        }
     #ifdef  __cplusplus
    }
    #endif
    

    c++中

    extern "C"{
        void printfs(){
            printf("gogo\n");
        }
       } 
    

    c中

        void printfs(){
            printf("gogo\n");
        }
    

    14 C++中为何要构造的时候基类先构造,子类后构造,析构的顺序相反呢?

    继承,使用父类的成员
    因为子类很有可能会用到从父类继承来的成员.
    析构顺序这样安排的原因在于子类有可能使用父类的成员,要进行释放操作

    虚函数表指针的偏移量是如何计算出来的?如何真正找到我想到访问的函数?

    对象实例化的地址得到虚函数表的地址。
    在这里插入图片描述

    如上图所示,虚函数表用来存储虚函数,而虚函数表指针是虚函数指针的指针,存放在对象的头部,通过对象实例化的地址得到虚函数表的地址。
    虚函数表指针vptr指向的是第一个虚函数,而不是虚函数表首地址
    计算;32位系统占4个字节,64位占8个字节,以后者为例:
    第一个虚函数的地址=虚函数表指针的地址=虚函数表的首地址+16字节偏移
    正常函数是强⾏计算地址;虚函数时通过偏移+的⽅式

    16 C++ 的重载和重写是如何实现的

    这个在静态多态里提过,重载是在编译期间完成的,通过命名倾轧技术,也就是根据函数形参的类型和顺序对函数重命名,可能不同编译器的命名标准不一样。

    而重写则需要在基类函数加入virtual关键字,在派生类中重写该函数,根据作用域的不同调用不同类中的函数,具体实现涉及到虚表(类),虚函数指针,虚表指针(对象),这个在前面(虚函数表指针的偏移量是如何计算出来的?如何真正找到我想到访问的函数?)提过。

    c++中的函数重载、函数重写、函数重定义 - PRO_Z - 博客园 (cnblogs.com)

    (190条消息) typedef void (*Fun) (void) 的理解——函数指针——typedef函数指针_走在不归路上的博客-CSDN博客_typedef void

    17 override 和 overload区别呢?

    本质区别在于,override(重写)修饰的方法在调用的时候只有一个方法被使用

    原因在于override一般用在子类继承父类,重写父类的方法,与父类中的某个方法的名称和参数完全相同

    1. 重写⽅法的参数列表,返回值,所抛出的异常与被重写⽅法⼀致
    2. 被重写的⽅法不能为private
    3. 静态⽅法不能被重写为⾮静态的⽅法
    4. 重写⽅法的访问修饰符⼀定要⼤于被重写⽅法的访问修饰符(public>protected>default>private)

    overload是重载,这些方法的名称相同而参数形式不同

    规则

    1. 不能通过访问权限、返回类型、抛出的异常进⾏重载
    2. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不⼀样)
    3. ⽅法的异常类型和数⽬不会对重载造成影响

    18 请你回答一下 C++ 类内可以定义引用数据成员吗?

    初始化需要特殊处理

    可以,但有三个条件

    需要在初始化列表中初始化,初始化列表能只能初始化一次,而非构造函数

    构造函数的形参必须是引用类型

    必须自己提供构造函数来初始化成员变量。

    19 简述一下什么是常函数,有什么作用

    常函数

    T:普通成员函数形参列表后面加上const的修饰符。

    构造函数不能为常函数,其本身用于成员的的初始化,析构同理

    全局函数和静态成员函数也不行,函数体没有this指针

    函数内this只能对成员变量进行读取而不能修改。

    如果想要对成员变量进行修改,需要mutable关键字修饰

    构造函数和析构函数不能为常函数。全局函数和静态成员函数也不能。

    常对象只能调用常函数,只能读成员,mutable除外

    class Person {
    public:
        int m_Age;
        mutable int m_Height;
        Person(int age) {
            this->m_Age = age;
        }
        // this指针的本质  指针常量   Person* const this;
        // 如果想让this指针指向的对象的内容不能被修改,const Person* const this;
        void show() const{        // 常函数
            // this->m_Age = 200;    // 常函数中,不能对成员进行修改
            this->m_Height = 180;    // 常函数中,可以修改mutable修饰的成员
            cout << this->m_Age << endl;
        }
    
        void show1() {}
    };
    
    int main() {
    
        Person p1(20);
        p1.show();
    
        // 常对象
        const Person p2(30);    // 常对象
        // p2.m_Age = 20;    // 常对象不能修改内容
    
        p2.m_Height = 190;    // 常对象可以修改mutable修饰的成员变量
    
        p2.show();    // 常对象可以调用常函数
        // p2.show1();    // 常对象不能调用普通函数
    
        return 0;
    }
    

    常量对象

    作用在于常量对象可以调用常函数,而不能调用非常函数;另外这也是类设计的一种限定

    20 C++派生类对象的初始化顺序,存在多个基类。

    基类构造函数(按照在派生类出现的顺序构造)→成员类对象构造函数→派生类自身构造函数

    21 简述下向上转型和向下转型

    向上转型:子类转换为父类,使用dynamic_cast(expression),安全,数据不易丢失

    向下转型: 父类转换为子类,可以使用强制转换,这种转换不安全,会导致数据的丢失

    22 只定义析构函数,会自动生成哪些构造函数

    编译器会自动生成拷贝构造和默认构造函数

    23 一个空类,默认会生成哪些函数

    • 无参构造函数-对象初始化;

    • 拷贝构造函数-复制对象

    • 赋值运算符重载

    Empty& operator = (const Empty & copy)
    {
    }
    
    • 析构函数(非虚)

    24 为何静态成员函数无法对对象中非静态成员进行访问?

    不属于任何一个对象

    因为对象在调用非静态成员函数时,系统会把对象的初始指针赋给成员函数的this指针,而静态成员函数不属于任何一个对象,也就没有this指针,所以无法访问非静态成员

    c++11可变参数模板

    在这里插入图片描述

    概念、语法、模板参数包、展开参数包

    在 C++11 之前,类模板和函数模板只能含有固定数量的模板参数。C++11 增强了模板功能,它对参数进行了高度泛化,允许模板定义中包含 0 到任意个、任意类型的模板参数,这就是可变参数模板。可变参数模板的加入使得 C++11 的功能变得更加强大,能够很有效的提升灵活性。

    深拷贝与浅拷贝的区别

    而且最根本的区别在于能够真正获取一个对象的复制实体,而不是引用,一般在拷贝构造函数和赋值运算符重载函数中涉及到。

    默认一般是浅拷贝,如果源对象的成员在堆区里面开辟了一片内存,那么在析构函数释放的时候,二者指向相同的空间,如果是浅拷贝的话,会导致多次释放。

    浅拷贝

    如何定义一个只能在堆上(栈上)生成对象的类?

    关键字: 静态,动态,私有析构函数,重载new,delete

    1、只能建立在堆上

    类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。

    容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。

    当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

    因此,将析构函数设为私有,类对象就无法建立在栈上了。代码如下:

    class A
    {
    public:
        A(){}
        void destory(){delete this;}
    private:
        ~A(){}
    };
    

    试着使用A a;来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。

    上述方法的一个缺点就是,无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。

    另一个问题是,类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:

    class A
    {
    protected:
        A(){}
        ~A(){}
    public:
        static A* create()
        {
            return new A();
        }
        void destory()
        {
            delete this;
        }
    };
    

    这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。

    2、只能建立在栈上

    只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。代码如下:

    
    
    class  A  
    {  
    private :  
        void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
        void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
    
    
    
    public :  
        A(){}  
        ~A(){}  
    }; 
    

    如何定义一个只能在堆上(栈上)生成对象的类?

    关键字: 静态,动态,私有析构函数,重载new,delete

    1、只能建立在堆上

    类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。

    容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。

    当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

    因此,将析构函数设为私有,类对象就无法建立在栈上了。代码如下:

    class A
    {
    public:
        A(){}
        void destory(){delete this;}
    private:
        ~A(){}
    };
    

    试着使用A a;来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。

    上述方法的一个缺点就是,无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。

    另一个问题是,类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:

    class A
    {
    protected:
        A(){}
        ~A(){}
    public:
        static A* create()
        {
            return new A();
        }
        void destory()
        {
            delete this;
        }
    };
    

    这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。

    2、只能建立在栈上

    只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。代码如下:

    
    
    class  A  
    {  
    private :  
        void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
        void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
    
    
    
    public :  
        A(){}  
        ~A(){}  
    }; 
    
  • 相关阅读:
    java实习生面试题会怎么问?Java常用面试题总结及答案
    wins10安装ffmpeg
    oracle学习43-oracle导出空表
    【结构型】桥接模式(Bridge)
    http和https包解析
    binding 里面local的用法
    java-Arrays
    Java学习 --- 类方法(静态方法)
    激光雷达物体检测(二):点视图检测算法
    Mysql整理-SQL语言
  • 原文地址:https://blog.csdn.net/qq_37087723/article/details/126829452