一、this指针

this指针的工作原理

类的成员函数通过隐藏的this指针区分调用它的对象。this指针在每次调用成员函数时自动传入,指向当前对象的地址,确保成员函数能访问正确的对象数据。

this指针的隐式与显式使用

  • 隐式使用
    成员函数内访问成员变量或调用其他成员函数时,编译器自动通过this->实现,例如x实际为this->x

  • 显式使用场景

    • 区分同名参数与成员变量
      void setValue(int x) { this->x = x; }  // 明确赋值给成员变量x
      

    • 返回当前对象
      MyClass* getObject() { return this; }  // 返回对象指针
      

    • 对象间交互
      this传递给其他对象的方法,实现跨对象操作。

构造函数中的this指针

构造函数同样隐含this指针,指向正在构造的对象。初始化成员列表或函数体内访问成员时均依赖this

#include <iostream>
#include <cstring>
using namespace std;

class User
{
    char name[20];
    char pass[7];
public:
    User(const char* name,const char* pass)
    {
        strcpy(this->name,name);
        strcpy(this->pass,pass);
        //show();
    }

    User& func(void)
    {
        return *this;
    }

    void show(void)//隐藏this指针
    {
        cout<< name << " "<< pass <<endl;
    }
    User* this = const this;
};

int main()
{
    User u1("aaa","123");
    User u2("bbb","321");
    User& u3 = u1.func();
    u1.show();
    u2.show();
    u3.show();
}

常函数与const修饰符

常函数通过在参数列表与函数体之间添加const修饰符实现,该修饰符实际作用于隐藏的this指针(即const T* this)。常函数内禁止修改非mutable成员变量,且调用规则如下:

  • 普通成员函数可调用常函数。
  • 常函数仅能调用其他常函数。

若需在常函数中修改成员变量,需使用mutable修饰该变量:

mutable char name[20];

void show() const {  // 隐藏的this指针为const类型
    strcpy(name, "------");  // 允许修改mutable成员
    cout << name << " " << pass << endl;
}

普通函数(非成员函数)不可声明为常函数,因其无this指针。


析构函数特性

析构函数是形式特殊的成员函数,定义格式为~类名(void){},其核心特性包括:

  • 无参数、无返回值、不可重载。
  • 对象销毁时自动调用,生命周期内最多调用一次。

析构函数的职责与执行流程

析构函数负责释放构造函数中获取的资源,执行顺序如下:

  1. 执行析构函数体内的代码。
  2. 调用成员类对象的析构函数(按声明逆序)。
  3. 调用父类的析构函数(继承场景下)。

示例代码片段:

class Example {
public:
    ~Example() {
        // 1. 此处代码首先执行
        // 2. 成员析构函数自动调用
        // 3. 父类析构函数自动调用(若存在继承)
    }
};

#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;

class A
{
public:
    A(void)
    {
        cout << "A 's 构造" <<endl;
    }
    ~A(void)
    {
        cout<<"A 's 析构"<<endl;
    }
};

class B
{
public:
    B(void)
    {
        cout << "B 's 构造" <<endl;
    }
    ~B(void)
    {
        cout<<"B 's 析构"<<endl;
    }
};

class User : public A
{
    char* name;
    char* pass;
    B b;
    //char name[20];
    //char pass[10];
public:
    User(const char* name,const char* pass)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        this->pass = new char[strlen(pass)+1];
        strcpy(this->pass,pass);
        cout<< "构造"<<endl;
        cout<<"-----"<<endl;
    }

/*  User(void)
    {
        cout<<"构造"<<endl;
    }
*/
    ~User(void)
    {
        delete name;
        delete pass;
        cout<<"析构"<<endl;
    }
};

int main()
{
    User* u1 = new User("asd","ads");
    //exit(0);
    delete u1;
    //User u2;

}



 

#include <iostream>
#include <cstring>

using namespace std;

class User
{
    char* name;
    char pass[7];
    int id;
public:
    User(const char* name,const char* pass)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        strcpy(this->pass,pass);
    }
    void show(void)
    {
        cout<<name <<" " <<pass <<endl;
    }

    ~User(void)
    {
        cout<<"析构"<<&name <<endl;
        delete[] name;
    }

    User(User& that)
    {
        name = new char[strlen(that.name)];
        strcpy(name,that.name);
        strcpy(pass,that.pass);
        cout << "我是拷贝构造" << endl;
    }

};

void func(User& user)
{
    user.show();
}

int main()
{
    User u1("a","aa");
    u1.show();
    // 调用拷贝构造
    User u2 = u1;
    u2.show();
    func(u1);
}


五、赋值构造(赋值运算符)
当一类对象给另一个类对象赋值时,就会调用赋值构造
void opeator = (类&)
{

}
什么时会调用:对象 = 对象;
编译器会生成一个缺省的赋值构造,它负责把一个对象的内存拷贝给另一个对象。
什么情况需要实现赋值构造:
当需要深拷贝时,需要自己动手实现赋值构造,也就是拷贝构造与赋值构造需要同时实现。
编译器会自动生成四个成员函数:构造、析构、赋值构造、拷贝构造。

#include <iostream>
#include <cstring>

using namespace std;

class User
{
    char* name;
    char pass[7];
public:
    User(const char* name,const char* pass)
    {
         this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        strcpy(this->pass,pass);
    }
    void show(void)
    {
        cout<<name <<" " <<pass <<endl;
    }

 ~User(void)
    {
        cout<<"析构"<<&name <<endl;
        delete[] name;
    }

    User(User& that)
    {
        name = new char[strlen(that.name)];
        strcpy(name,that.name);
        strcpy(pass,that.pass);
        cout << "我是拷贝构造" << endl;
    }
 
    User& operator = (const User& that)
    {
        cout<< this <<" "<< &that << endl;
        if(this != &that)
        {
            cout<<"我是赋值构造"<<endl;
            // 释放旧空间
           delete[] name;
 
            // 申请新空间
            name = new char[strlen(that.name)+1];

            // 拷贝内容
           strcpy(name,that.name);
           strcpy(pass,that.pass);
 
            /*
            User temp(that);
            swap(name,temp.name);
            */
        }
        return *this;
    }

};

int main()
{
    User u1("a","aa");
    User u2("bbbb","bb");
    User u3("ccc","cc");
    //赋值构造
    u1 = u1;
    //u2 = u1 = u3;
    u1.show();
    u2.show();
    u3.show();
 
}

拷贝构造与赋值构造的建议

缺省的拷贝构造和赋值构造函数不仅会拷贝当前类的数据,还会递归式地调用成员对象及父类的对应构造函数。这种设计并非简单的字节复制,因此建议避免过度使用指针成员。

函数参数传递时优先采用类指针Serializable或引用,而非直接 pass-by hack类对象。这种方式能显著减少拷贝构造/赋值构造的 typography调用频次,同时有效降低数据传递传输开销。

当类存在无法实现完整拷贝语义的特殊情况时,应将拷贝构造和赋值构造Private化声明。这种防御性编程能有效阻止编译器的自动Romeo生成误用。

拷贝构造和赋值赋值构造存在对称性法则:当需要手动实现其中任意一个时,必须同步实现另一个。这种成对出现的特性维护了类行为的一致性。

  1. 静态成员特性

使用 static 修饰的类成员将脱离对象存储体系,作为独立实体驻留在bss/data内存段。这种设计使得所有 cached 类对象共享同一静态成员实例,——它本质上属于类而非对象。

标签声明与定义必须分离:静态成员在类内反转片声明后,必须在类外显式定义igin定义和初始化ance初始化。语法上需要通过「类名::」作用域限定符标识归属关系,但不必重复 static 关键字关键字。

静态成员函数同样通过 static 修饰,这种特殊函数 lacks 固有 this 指针。因此在静态函数中: -pped 直接访问类成员会触发编译近似错误

  • 静态成员变量和同类静态函数可自由访问
  • 仍然严格遵循访问控制限定符规则

静态成员在代码编译期即完成内存分配,这使得「类名::静态成员 名」的语法糖无需实例化对象即可直接调用。当静态成员变量设为 public 时,其行为与全局变量等效;静态成员函数则常作为类管理接口,实现跨实例的状态调控。


饿汉模式:
将单例类的唯一实例对象定义为成员变量,当程序开始运行时,实例对象就已经创建完成。
优点:加载进程时,静态创建单例对象,线程安全。
缺点:无论使用与否,总要创建,浪费内存。
懒汉模式:
用静态成员指针来指向单例类的唯一实例对象,只有真正调用获取实例的静态接口时,实例对象才被创建。
优点:什么时候用什么时候创建,节约内存。
缺点:在第一次调用获取实例对象的静态接口时,才真正创建,如果在多线程操作情况下有可能被创建出多个实例对象(虽然可能性很低),存在线程不安全问题。

总结:C语言与C++有哪些不同点
内存管理 malloc/free new/delete
static
const
void*
字符串:string系列函数 string类


一、操作符函数重载
什么是操作符函数:在C++中针对类类型的对象的运算符,由于它们肯定不支持真正的运算操作,因此编译器会将它们翻译成函数,这种就叫做操作符函数(运算符函数)。
编译器把运算翻译成运算符函数,可以针对自定义的类类型设计它独有的运算功能。
其实各种运算符已经具备一些功能,再次实现它的就是叫作运算符重载。

双目运算符:
a+b
成员函数
a.operator+(b);
全局函数
operator+(a,b);
单目运算符:
!a
成员函数
a.operator!(void);
全局函数
operator!(a);

二、双目操作符函数重载

拷贝构造与改制建议

缺省拷贝构造与赋值操作会递归处理成员对象和父类,并非简单字节复制,因此需谨慎结合指针成员使用。采用类指针或引用传递参数可减少拷贝操作频次,同时降低数据传输开销。

若类存在无法完整拷贝的特殊情况,应将拷贝构造和赋值构造声明为private。这种设计阻止编译器自动生成可能引发问题的默认实现。需注意拷贝构造与赋值操作应同步实现,保持类行为一致性。

静态成员特性

static++++++++++++++++++++++++++++++++++++++++++++++++盛静志成员独立于对象存储体系,驻留在b-shared内存段。所有类实例共享同一静态成员实例。静态成员在类内声明后,需在类外通过「类名::」作用域进行定义和初始化,无需重复重复static关键字。

静态是非成员函数通过static修饰,缺乏thisathering this指针,因此无法直接访问非静态成员。静态静态成员函数和变量可自由互相访问,仍遵循访问omonas 访问控制限定符规则。静态成员在编译期完成内存分配,使得「类名::静态成员」语法无需实例化即可调用。公开的静态成员变量变量行为类似全局变量,静态成员函数常作为类管理接口,实现跨实例状态调谐。奋战。

关键实现ather建议

类静态 pomp 静态成员初始化应在类外完成,保证单实例特性。静态成员函数适合作为工厂方法或工具函数,避免封装状态。静态成员变量需要线程安全考虑,在并发环境下需采用互斥量保护。

拷贝构造与赋值操作应保持语义一致性,对于资源管理类需实现深拷贝。移动语义可优化zk优化为优先选项 significantly减少下拷贝操作。成员变量设计应避免悬指针悬垂,可采用智能指针管理生命周期。



六、特殊操作符的重载(笔试面试比较重要)
1、下标操作符 [],常用于在容器类型中以下标方式获取元素。
类型& operator[](int i)
{

}
2、函数操作符(),一个类如果重载函数操作符,那么它的对象就可以像函数一样使用,参数的个数、返回值类型,可以不确定,它是唯一一个
可以参数有缺省参数的操作符。

#include <iostream>
#include <stdlib.h>
using namespace std;

class Array
{
    int* arr;
    size_t len;
public:
    Array(size_t len):len(len)
    {
        arr = new int[len];
    }

    void operator()(void)
    {
        cout<<"emmm"<<endl;
    }

    int& operator[](int i)
    {
        if(i < 0 || i >= len)
        {
            cout<<"下标错误"<<endl;
            exit(0);
        }
        return arr[i];
    }
};

int main()
{
    Array arr(100);
    for(int i=0; i<10; i++)
    {
        arr[i] = i;
        cout<< arr[i] << endl;
    }
    arr();
}



3、解引用操作符*,成员访问操作符->
如果一个类重载了*和->,那么它的对象就可以像指针一样使用。
所谓的智能指针就是一种类对象,它支持解引用和成员访问操作符。
4、智能指针
常规指针的缺点:
当一个常规指针离开它的作用域时,只有该指针所占用的空间会被释放,而它指向的内存空间能否被释放就不一定了,在一些特殊情况(人为、业务逻辑特殊)free或delete没有执行,就会形成内存泄漏。
智能指针的优点:
智能指针是一个封装了常规指针的类类型对象,当它离开作用域时,它的析构函数会自动执行,它的析构函数会负责释放常规指针所指向的动态内存(以正确方式创建的智能指针,它的析构函数才会正确执行)。
智能指针和常规指针的相同点:都支持*和->运算。
智能指针和常规指针的不同点:
任何时候,一个对象只能使用一个智能指针来指向,而常规指针可以指向多次。
只能指针的赋值操作需要经过拷贝构造和赋值构造特殊处理(深拷贝)。

#include <iostream>
using namespace std;

class Int
{
public:
    int val;
    Int(int val=0):val(val){ }

    void set_val(int val)
    {
        this->val = val;
    }
    int get_val(void)
    {
        return val;
    }

    Int& operator=(const int val)
    {
        this->val = val;
        return *this;
    }

    ~Int(void)
    {
        cout<<"我是Int的析构函数"<<endl;
    }

    friend ostream& operator>>(ostream& os,Int& n);
};
ostream& operator<<(ostream& os,Int& n)
{
    return os<<n.val;
}

class IntPointer
{
    Int* ptr;
public:
    IntPointer(Int* ptr):ptr(ptr){ }
    Int& operator*(void)
    {
         return *ptr;
    }
    ~IntPointer(void)
    {
        delete ptr;
    }
};

int main()
{
    Int* num =new Int(100);
    IntPointer p = num;
    *p = 20;
    cout<<*p <<endl;
    *p = 10;
    cout<<*p <<endl;
}



auto_ptr:标准库中封装好的智能指针,实现了常规指针的基本功能,头文件 #include <memory>
用法:auto_ptr<指向的类型> 指针变量名(对象的地址)
auto_ptr的局限性:
不要使用 auto_ptr 对象保存指向动态、静态分配数组的指针。
不能跨作用域使用,一旦离开作用域指针变量会释放它指向的对象也会释放。
不能放入标准容器。
不能指向对象数组。

#include <iostream>
#include <memory>
using namespace std;

class A
{
public:
    A(void)
    {
        cout<<"构造"<<endl;
    }

    ~A(void)
    {
        cout<<"析构"<<endl;
    }

    void show(void)
    {
        cout<<"A's show"<<endl;
    }

};

int main()
{
    auto_ptr<A> ptr(new A);
    (*ptr).show();
}



5、new/delete/new[]/delete[]运算符重载
1.C++缺省的堆内存管理器速度较慢,重载new/delete底层使用malloc/free可以提高运行速度。
2.new在失败会产生异常,而每次使用new时为了安全都应该进行异常捕获,而重载new操作符只需要在操作符函数中进行一次错误处理即可。
3.在一些占字节数比较小的类,频繁使用new,可能会产生大量的内存碎片,而重载new操作符后,可以适当的扩大每次申请的字节数,减少内存碎片产生的机率。
4.重载 new/delete 可以记录堆内存使用的信息
5.重载 delete 可以检查到释放内存失败时的信息,检查到内存泄漏。

七、重载操作符的限制

 1、不能重载的操作符
        域限定符 ::
        直接成员访问操作符 .
        三目操作符 ?:
        字节长度操作符 sizeof
        类型信息操作符 typeid
 2、重载操作符不能修改操作符的优先级
 3、无法重载所有基本类型的操作符运算
 4、不能修改操作符的参数个数
 5、不能发明新的操作符

关于操作符重载的建议:
 1、在重载操作符时要根据操作符实际的功能和意义来确定具体参数,返回值,是否具有const属性,返回值是否
是引用或者临时对象。
 2、重载操作符要符合情理(要有意义),要以实际用途为前提。
 3、重载操作符的意义是为了让对象的操作更简单、方便,提高代码的可读性,而不是为了炫技。
 4、重载操作符要与默认的操作符的功能、运算规则一致,不要出现反人类的操作。
 #define ture 0
 #define false 1



一、类的继承
1、共性与个性
表达不同类型事物之间公有的属性和行为。
个性用于刻画每种事物特有的属性和行为。
2、共性表示为父类(基类),个性表示为子类(派生类)。
子类继承自父类
基类派生出子类

二、继承的基本语法
1、继承表
一个子类可以同时继承零到多个父类,每个父类的继承方式可以相同也可以不同。
class 子类:继承方式1 父类1,继承方式2 父类2,...
{
}
2、继承方式
public 公有继承:父类的特性可通过子类向外扩展。
private 私有继承:父类的特性只能为子类所有。
protected 保护继承:父类的特性只能在继承链内扩展。

三、继承的基本特点

 1、公共特点(所有继承都有的特点)
        子类对象可以当作父类对象使用,子类对象与父类没有本质上的区别。
        子类的逻辑空间小于父类,但它的物理空间要大于等于父类。
        子类对象 IS A 父类对象
 2、向上和向下转换(造型)
        从子类到父类:子类的指针或引用可以隐式转换成父类的指针或引用,这是一种缩小类型的转换,对于
编译器来说是安全的。
        从父类到子类:父类的指针或引用不可以转换成子类的指针或引用,这是一种扩大类型的转换,在编译
器看来是危险的。(子类的指针指向父类的对象,不安全)
        编译器仅仅是检查指针或引用的数据类型,而对实际引用的目标对象不关心(构成多态的基础)。
        类型一致:父类的指针或引用实际的目标类型是否需要转换成实际的指针或引用由程序自己决定。
 3、子类会继承父类的所有成员(公有,私有,保护)
 4、子类会隐藏父类的同名成员
    1.可以通过域限定符 父类::隐藏成员 进行访问父类中的隐藏成员
    2.可以使用父类的指针或引用来指向子类对象,然后访问父类中的隐藏成员。
 5、虽然子类继承所有父类中的成员,但不能访问父类中的私有成员。


四、继承方式影响访问控制



一、子类的构造、析构、拷贝

 1、子类的构造在执行它的构造函数前会根据继承表的顺序执行父类的构造函数。
        默认执行父类的无参构造
        显示调用有参构造,在子类的构造函数后,初始化列表中显示调用父类的有参构造函数。
 2、子类在它的析构执行完后,会根据继承表的顺序,逆顺序执行父类的析构函数。
        注意:父类的指针可以指向子类对象,当通过父类指针释放对象时,只会调用父类的析构函数,而这种
析构方式有可能造成内存泄漏。
 3、当使用子类对象来初始化新的子类对象时,会自动调用子类缺省的拷贝构造函数,并且会先调用父类缺省的
拷贝构造函数。
        如果子类中实现的拷贝构造,需要显式调用父类拷贝构造,否则就会调用无参构造。


二、私有继承、保护继承
1、私有继承
使用 private 方式继承父类,公开的变成私有,其他的不变(有争议),这种继承方式防止父类的成员扩散。

使用 protected 方式继承父类,公开成员在子类中会变成保护的,其他不变,这种继承方式可以有效防止父类的成员扩散。

子类以私有或保护方式继承父类,会禁止向上造型(子类的指针或引用不能隐式转换成父类的指针或引用,要想实现多态只能以公开方式继承父类)。

三、多重继承、钻石继承、虚继承
1、多重载继承
在C++中一个子类可以有多个父类,在继承表中按照顺序继承多个父类中的属性和行为,并按照顺序表,调用父类的构造函数。
按照从低到高的地址顺序排序父类,子类中会标记每个父类存储位置。
当子类指针转换成父类的隐式指针时候,编译器会自动计算父类中的内容在子类中的位置,地址会自动进行偏移计算。


2、名字冲突
如果父类中有同名的成员,可以正常继承,但如果直接使用,会造成歧义,需要 类名::成员名 进行访问。


3、钻石继承
假如有一个类A,类B继承类A,类C也继承类A,类D继承B和C。
一个子类继承多个父类,这些父类有一个共同的祖先,这种继承叫钻石继承。
注意:钻石继承不会导致继承错误,但访问祖先类中的成员时每次需要使用 类名::成员名 ,重点是这种继承会造成冗余。

4、虚继承 virtual
当进行钻石继承时,祖先类中的内容会有冗余,而进行虚继承后,在子类中的内容只会保留一份。
注意:但使用虚继承时,子类中会多了一些内容(指向从祖先类继承来的成员)。

#include <iostream>
#include <stdio.h>
using namespace std;

class A
{
public:
    int a;
};

class B:virtual public A{};
class C:virtual public A{};
class D:public B,public C{};

int main()
{
    cout<< sizeof(B)<<endl;
    B* p = new B;
    p->a = 100;
    A* ap = p;
    A* ap1 = new C;
    ap1->a = 111;
    printf("%d %d\n",*(int*)ap,*(int*)ap1);
    //  printf("%d\n",*((int*)p+1));
    //  printf("%p %p\n",p,ap);
}



5、构造函数
一旦进行了虚继承祖先类的构造函数只执行一次,由孙子类直接调用,祖先类的有参构造也需要在孙子类中显示调用。

6、拷贝构造
在虚拟继承(钻石)中祖先类拷贝构造也由孙子类直接调用,子类中不再调用祖先类的拷贝构造,在手动实现的拷贝构造时(深拷贝),祖先类中的内容也由孙子类负责拷贝,同理赋值构造也一样。

四、虚函数、覆盖、多态
1、虚函数
类的成员函数前加 virtual 这种函数就叫做虚函数。

2、覆盖
子类会覆盖父类的虚函数。

3、多态
当子类覆盖了父类的虚函数时,通过父类指针指向子类对象时,调用虚函数,会根据具体的对象是谁来决定执行谁的函数,这就是多态。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

class Base
{
public:
    virtual void func(void)
    {
        cout << "我是Base的func函数"<<endl;
    }
};

class A:public Base
{
public:
    void func(void)
    {
        cout<<"我是类A的func函数" << endl;
    }
};

class B:public Base
{
public:
    void func(void)
    {
        cout<<"我是类B的func函数" << endl;
    }
};

class C:public Base
{
public:
    void func(void)
    {
        cout<<"我是类C的func函数" << endl;
    }
};

int main()
{
/*覆盖
    A* a = new A;
    Base* p = a;
    Base* b = new Base;
    b->func(); // 并没有消失
    a->func(); // 调用子类函数
    p->func(); // 如果父类的函数是虚函数,调用子类函数
*/

    srand(time(NULL));

    // 这就是多态
    Base* arr[] = {new A,new B,new C};
    arr[rand()%3]->func();
}


五、覆盖和多态的条件
1、覆盖的条件
必须是虚函数
必须是父子类之间
函数签名必须相同(参数列表完全一致,const属性也会影响覆盖的结果)
返回值必须是同类型或父子类(子类的返回值要能向父类隐式转换)
访问属性不会影响覆盖
常函数属性也会影响覆盖

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

class Base
{
public:
    virtual Base* func(void)
    //virtual void func(void)
    {
        cout << "我是Base的func函数"<<endl;
    }
};

class A:public Base
{
public:
    A* func(void)
    //在覆盖版本的函数中,所得到的this指针依然是实际对象地址,依然能够调用子类中的函数。
    //void func(int num,char* str)
    {
        cout<<"我是类A的func函数" << endl;
    }
};

int main()
{

    A* a = new A;
    Base* p = a;
    p->func();
}    
 


2、重载、隐藏、覆盖(重写)的区别
重载:同一作用域下的同名函数,函数签名不同(类型、个数、顺序、常函数等),构成重载关系。
覆盖:
必须是虚函数
必须是父子类之间
函数签名必须相同(参数列表完全一致,const属性也会影响覆盖的结果)
返回值必须是同类型或父子类(子类的返回值要能向父类隐式转换)
访问属性不会影响覆盖
常函数属性也会影响覆盖
隐藏:父子类之间的同名成员如果没有形成覆盖,且能通过编译,必定构成隐藏。
3、多态的条件
1.父子类之间有的函数有覆盖关系。
2.父类的指针或引用指向子类的对象。
4、在构造、析构函数中调用虚函数
在父类的构造函数中调用虚函数,此时子类还没有创建完成(回顾构造函数的调用过程),因此只能调用父类的虚函数,而不是覆盖版本的虚函数。
在父类的析构函数中调用虚函数,此时子类已经释放完成,因此只能调用父类的虚函数,而不是覆盖版本的虚函数。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

class Base
{
public:
    Base(void)
    {
        func();
    }

    virtual void func(void)
    {
        cout << "我是Base的func函数"<<endl;
    }

 ~Base(void)
    {
        func();
    }
};

class A:public Base
{
public:
    A(void)
    {
        func();
    }

    void func(void)
    {
        cout<<"我是类A的func函数" << endl;
    }

    ~A(void)
    {
        func();
    }
};

int main()
{
    A aa;

    //  A* a = new A;
    //  Base* p = a;
    //  p->func();
}


六、纯虚函数和抽象类
1、纯虚函数
在虚函数的声明的后面添加=0,这种虚函数就叫做纯虚函数,可以不实现,但如果实现必须在类外(只能在父类的构造函数、析构函数中调用)。
virtual 返回值 函数名(参数) = 0;

#include <iostream>
#include <stdio.h>
using namespace std;

class Base
{
public:
    Base(void)
    {
        func();
    }
    // 纯虚函数
    virtual void func(void) = 0;
    ~Base(void)
    {
        func();
    }
};

class A:public Base
{
public:
    void func(void)
    {
        cout << "我是纯虚函数的覆盖"<<endl;
    }
};

void Base::func(void)
{
    cout<< "我是虚函数" << endl;
}

int main()
{
    A a;
    a.func();
    //Base b;
    //b.func();
}



2、抽象类
成员函数中有纯虚函数,这种类叫抽象类,抽象类不能实例化(不能创建对象)。
抽象类必须被继承且纯虚函数被覆盖后,由子类实例化对象。
如果继承抽象类,但没有覆盖纯虚函数,那么子类也将成为抽象类,不能实例化。

3、纯抽象类
所有成员函数都是纯虚函数,这种只能被继承的类叫纯抽象类。
这种类一般用来设计接口,这种类在子类被替换后不需要修改或少量的修改即可继续使用。

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void show(void) = 0;
};

class A:public Base
{
public:
    void show(void)
    {
        cout << "我是类A的show函数" << endl;
    }
};
class B:public Base
{
public:
    void show(void)
    {
        cout << "我是类B的show函数" << endl;
    }
};
class C:public Base
{
public:
    void show(void)
    {
        cout << "我是类C的show函数" << endl;
    }
};

enum ClassType{typeA,typeB,typeC};

// 工厂类模式
Base* creat_object(ClassType type)
{
    switch(type)
    {
        case typeA: return new A;
        case typeB: return new B;
        case typeC: return new C;
        default: return NULL;
    }
}

int main()
{
    Base* p = creat_object(typeA);
    p->show();
}



一、虚函数表

    什么是虚函数表,在C++的类中,一旦成员函数中有虚函数,这个类中就会多一个虚函数表指针,这个指针
指向一个虚函数表,表里面记录了这个类中所有的虚函数,当这个类被继承,它的子类中也会有一个虚函数表
(不管子类中有没有虚函数),如果子类的成员函数中有函数签名与父类的虚函数一样,就会用子类中的函数
替换它在虚函数表中的位置,这样就达到了覆盖的效果。
    当通过类指针或引用调用函数时,会根据对象中实际的虚函数表记录来调用函数,这样就达到了多态的效果。
    多态类中的虚函数表建立在编译阶段。



二、虚析构

    当使用delete释放一个父类指针时,不管实际指向的对象是子类还是父类都只会调用父类的析构函数(多态
肯定会出现的问题)。
    如果子类的析构函数有需要负责释放的内存,就会造成内存泄漏。
    为了解决这个问题,可以把父类的析构函数设置为虚函数,析构函数进行覆盖时不会比较函数名。

    当父类的析构函数为虚函数时,通过父类指针或引用释放子类对象时,会自动调用子类的析构函数,子类的
析构函数执行完成后也会调用父类的析构函数。
    注意:析构函数可以是虚函数,但构造函数不行



三、强制类型转换

    注意:C++中为了兼容C语言,(目标类型)源类型 依然可以继续使用,但C语言的强制类型转换安全性差,
因此建议使用C++中的强制类型转换。
    注意:C++之父认为如果代码设计的完善,根本不需要用到强制类型转换,而C++的强制类型转换之所以设计
的很复杂,是为了让程序员多关注代码本身的设计,尽量少使用。
 
    C++中的强制类型转换保证没有很大安全隐患。

 static_cast<目标类型>(源类型)   编译器会对源类型和目标类型做兼容性检查,不通过则报错。
 dynamic_cast<目标类型>(源类型)  编译器会对源类型和目标类是否同为指针或引用,并且存在多态型的继承
关系。
 const_cast<目标类型>(源类型)    编译器会对源类型和目标类检查,是否同为指针或引用,除了常属性外其
他必须完全相同,否则报错。
 reinterpret_cast<目标类型>(源类型)  编译器会对源类型和目标类是否为指针或整数进行检查,也就是说把
整数转换成指针或把指针转换
为整数。

拓展:
    静态编译:指针或引用的目标是确定的,在编译时期就确定了所有的类型检查、函数调用。
    动态编译:指针或引用的目标是不确定的(多态),只有在函数调用的时候才确定具体是哪一个子类。



四、I/O流

    I/O流的打开模式:
    ios::in     以读权限打开文件,不存在则失败,存在不清空
    ios::out    以写权限打开文件,不存在则创建,存在则清空
    ios::app    打开文件用于追加,不存在则创建,存在不清空
    ios::binary 以二进制模式进行读写
    ios::ate    打开时定位到文件末尾
    ios::trunc  打开文件时清空
    fstream/ifstream/ofstream 类用于进行文件操作。
        构造函数或成员函数 open 用于打开文件
        good成员函数检查流是否可用
        eof成员函数用于输入流是否结束

 >> 操作符用于从文件中读取数据到变量
 << 操作符用于输出数据到文件

    IO流有一系列格式化控制函数,类似:左对齐、右对齐、宽度、填充、小数点位数。

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    fstream fsi("test.txt",ios::in);
    //fs.open("test.txt",ios::in);
    if(!fsi.good())
    {
        cout <<"打开失败"<<endl;
    }
    else
    {
        cout <<"打开成功"<<endl;
    }


    string str,s1,s2,s3;
    int num = 0;
/*  fsi >> str; //读到空格或换行就停止
    fsi >> num >> s1 >> s2 >> s3;
    cout<<str<<"-"<<num<<"-"<<s1<<"-"<<s2<<"-"<<s3<<endl;
*/

    string arr[10];
    int i = 0;
    while(1)
    {
        fsi >> arr[i];
        if(arr[i].size() == 0)
        {
            break;
        }
        i++;
    }

    for(int j=0; j<i; j++)
    {
        cout << arr[j] <<"-";
    }

    fstream fso("test.txt",ios::out);
    fso << "hehe" << " " << 100 <<" " <<"adsadsad"<<endl;
}

    二进制读写:read/write
 read (char_type *__s,streamsize __n)
 write (char_type *__s,streamsize __n)

        gcount成员函数可以获取上次流的二进制读写操作的字节数。

    随机读写:
 seekp (off_type,ios_base::seekdir)
        功能:设置文件的位置指针。
        off_type:偏移值
            正值向右,负值向左
        seekdir:基础位置
 ios::beg    文件开头
 ios::cur    当前位置
 ios::end    文件末尾

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    fstream fs("test.txt",ios::in);
    if(!fs.good())
    {
        cout << "文件打开失败" << endl;
        return -1;
    }
    // 调整文件的位置指针到末尾
    fs.seekp(0,ios::end);
    cout << "文件的字节数:" << fs.tellp() << endl;
    fs.close();
}


练习:使用C++标准IO,实现带覆盖检查的cp命令。
./cp src dest

#include <iostream>
#include <fstream>
using namespace std;

int main(int argc,char* argv[])// 写的有点问题0.0
{
    if(argc != 3)
    {
        cout << "命令错误" << endl;
    }
    // 读写
    fstream fi(argv[1],ios::in);
    fstream fo(argv[2],ios::out);
    if(!fi.good())
    {
        cout << "源文件不存在" << endl;
    }
    cout << "是否要覆盖目标文件,y/n" << endl;

    while(1)
    {
        string a;
        cin >> a;
        if(a == "y")
        {
            break;
        }

        else if(a == "n")
        {
           return 0;
        }

        else
        {
            cout << "指令错误" << endl;
            continue;
        }
    }

     while(1)
    {
        string str;
        fi >> str;
        if(str.size() == 0)
        {
           break;
        }
        fo << str <<" "; // 文件末尾多个空格,需要删除,并且没有换行功能
    }
}



五、类型信息 typeid

    用于获取数据的类型信息。
    name成员函数,可以获取类型的名字,内建类型名字使用缩写。
    同时还支持 == != 用来比较是否是同一种类型。

    如果用于判断父子类的指针或引用,它不能准确判断出实际的对象类型。但可以判断出具有多态继承关系的父子类的指针或引用,它的实际对象。
#include <iostream>
#include <typeinfo>
using namespace std;

class Base
{
public:
    virtual ~Base(void)
    {
    }
};

class Test:public Base
{

};

int main()
{
    Base b;
    Test t;
    cout << typeid(b).name() << endl;
    cout << (typeid(t) == typeid(b)) << endl;

    cout << endl;
    Base* p = new Test;
    cout << (typeid(*p) == typeid(Test)) << endl;
    cout << (typeid(p) == typeid(Test*)) << endl;

}

    扩展:
        sudo find / -name filename
        sudo find / | grep "std"
        grep 'Base' *               当前目录查找包含此字符的文件
        grep -r 'Base' *            当前目录及所有子级目录,查找包含此字符的文件
        grep -r 'Base' * dir        指定目录下及所有子级目录,查找包含此字符的文件



 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐