#C++ 笔记二

news2024/11/23 17:25:13

 四、运算符重载

1.友元

1.1 概念

类实现了数据的隐藏和封装,类的数据成员一般定义为私有成员,仅能通过类的公有成员函数才能进行读写。

如果数据成员定义成公共的,则又破坏了封装性。但是在某些情况下,需要频繁的读写数据成员,特别实在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查都是需要时间开销的,而影响程序的运行效率。

友元有三种实现方式:

  • 友元函数
  • 友元类
  • 友元成员函数

友元函数是一种定义在类外部的普通函数,但是他需要在类内进行声明,为了和该的成员函数加以区分,在声明时前面加一个关键字friend。

友元不是成员函数,但是能够访问当前类中的所有成员(包括私有成员)。

友元在于提高的程序运行效率,但是它破坏了类的封装性和隐藏性,使得非成员函数能够访问类的私有成员,导致程序的维护性变差,因此使用友元要慎重。

1.2 友元函数(熟悉)

友元函数不属于任何一个类,但是可以访问类中所有的成员(包括私有成员)。

需要在类内进行声明。

#include <iostream> // 引入标准输入输出流库,用于控制台输入输出

using namespace std; // 使用标准命名空间,以便直接使用标准库中的对象和函数(如cout、endl)而无需加上std::前缀。

class Test
{
private:  // private访问控制符声明以下成员是私有的,即只能在类的内部访问。
    int a;
public:   // 声明以下成员是公共的,即可以从类外部访问。
   Test(int i):a(i){} // 是一个构造函数,用于创建对象时初始化成员变量a,参数i传递的值赋给a。

    void show()
    {
        cout << a << " " << &a << endl;
    }
    // 友元函数,类内声明
    friend void and_test(Test &t); // 可以访问类Test的私有成员。虽然声明在类内部,但定义在类外部。
};

// 友元函数定义
void and_test(Test &t)
{
    cout << t.a << endl;  // 打印传递对象t的私有成员a的值
    cout << ++t.a << " " << &t.a << endl;
}

int main()
{

    Test t1(1); // 创建一个名为t1的Test对象,并将a初始化为1
    and_test(t1); // 调用友元函数and_test,传递t1对象
    t1.show(); // 调用t1的show方法,打印t1的成员a的值及其内存地址
    return 0;
}

友元函数的使用需要注意以下几点:

  • 友元函数没有this指针。
  • 友元函数的“声明”可以放置到类中的任何位置,不受权限修饰符的影响。
  • 一个友元函数理论上来说可以访问多个类,只需要在各个类中进行声明。

1.3 友元类(掌握)

当一个类B成为了另一个类Test的朋友时,类Test的所有成员都可以被类B访问,此时类B就是类Test的友元类。

#include <iostream>

using namespace std;

class Test
{
private:
    int a;
public:
    Test(int i):a(i){}

    void show()
    {
        cout << a << " " << &a << endl;
    }
    // 友元类,类内声明
    friend class B;
};

class B
{
public:
    void and_test(Test &t)
    {
        cout << t.a << endl;
        cout << ++t.a << " " << &t.a << endl;
    }

    void and_test2(Test &t)
    {
        cout << t.a << endl;
        cout << ++t.a << " " << &t.a << endl;
    }
};


int main()
{
    Test t1(2);
    B b;
    b.and_test(t1);
    b.and_test2(t1);
    t1.show();
    return 0;
}

友元类的使用需要主要以下几点:

  • 友元关系不能被继承。
  • 友元关系不具有交换性(比如:类B声明成类Test的友元。类B可以访问类Test中的成员,但是类Test不能访问类B的私有成员,如果需要访问,需要将类Test声明成类B的友元,即互为友元)。

互为友元,需要类内声明,类外实现。

#include <iostream>

using namespace std;

class Cat;
class Test
{
private:
    int a;
public:
    Test(int i):a(i){}
    void test(Cat &c);
    friend class Cat;
};

class Cat
{
private:
    int b;
public:
     Cat(int i):b(i){}
    void test1(Test &t);
    friend class Test;
};

void Test::test(Cat &c)
{
    cout <<c.b<<endl;
}

void Cat::test1(Test &t)
{
    cout <<t.a++<<endl;
}

int main()
{
    Test t(44);
    Cat c(12);
    c.test1(t);
    return 0;
}

1.4 友元成员函数(熟悉)

使类B中的成员函数成为类Test的友元成员函数,这样类B的该成员函数就可以访问类Test所有的成员了。

#include <iostream>

using namespace std;

// 第四步:声明被访问的类
class Test;
class B
{
public:
    // 第二步:声明友元成员函数(类内声明,类外实现)
    void and_test(Test &t);
};

class Test
{
private:
    int a;
public:
    Test(int i):a(i){}
    void show()
    {
        cout << a << " " << &a << endl;
    }
    // 友元成员函数 第一步:确定友元函数的格式并声明
    friend void B::and_test(Test &t);
};


// 第三步:类外定义友元成员函数
void B::and_test(Test &t)
{
    cout << t.a << endl;
    cout << ++t.a << " " << &t.a << endl;
}


int main()
{
    Test t1(2);
    B b;
    b.and_test(t1);
    t1.show();
    return 0;
}

2、运算符重载(掌握)

2.1 概念

C++中可以把部分运算符看作是函数,此时运算符也可以重载。

运算符预定义的操作只能针对基本数据类型,但是对于自定义类型,也需要类似的运算操作。此时就可以重新定义这些运算符的功能,使其支持特定类型,完成特定的操作。

运算符重载有两种实现方式:

  • 友元函数运算符重载
  • 成员函数运算符重载

2.2 友元函数运算符重载

运算符表达式与函数的对应关系如下所示:

#include <iostream>

using namespace std;

class MyInt
{
private:
    int a;
public:
    MyInt(int a):a(a){}
    int get_int()
    {
        return a;
    }
    // +运算符重载 友元函数实现
    friend MyInt operator +(MyInt &i,MyInt &i2);
};

MyInt operator +(MyInt &i,MyInt &i2)
{
//    MyInt i3(0);
//    i3.a = i.a + i2.a;
//    return i3;
    // int → MyInt 隐式调用构造函数
    return i.a + i2.a;
}

int main()
{
    MyInt a1(1);
    MyInt a2(2);

    MyInt a3 = a1 + a2;
    cout << a3.get_int() << endl;

    return 0;
}

自增运算符重载

#include <iostream>

using namespace std;

class MyInt
{
private:
    int a;
public:
    MyInt(int a):a(a)
    {
//        cout << "构造函数" << endl;
    }

    int get_int()
    {
        return a;
    }
    // +运算符重载 友元函数实现
    friend MyInt operator +(MyInt &i,MyInt &i2);
    friend MyInt operator ++(MyInt &i); // 前置++
    friend MyInt operator ++(MyInt &i,int); // 后置++
};

MyInt operator +(MyInt &i,MyInt &i2)
{
//    MyInt i3(0);
//    i3.a = i.a + i2.a;
//    return i3;
    // int → MyInt 隐式调用构造函数
    return i.a + i2.a;
}

MyInt operator ++(MyInt &i)
{
    return ++i.a;
}

MyInt operator ++(MyInt &i,int)
{
    return i.a++;
}

int main()
{
    MyInt a1(1);
    MyInt a2(2);

    MyInt a3 = a1 + a2;
    cout << (++a3).get_int() << endl;  // 前置自增 4
    cout << (a3++).get_int() << endl;  // 4
    cout << (a3++).get_int() << endl;  // 5
    cout << a3.get_int() << endl;      // 6
    return 0;
}

2.3 成员函数运算符重载

成员函数运算符重载相比于友元函数运算符重载,最大区别在于,友元函数的第一个输入参数,在成员函数运算符中使用this指针代替。因此同样的运算符重载,成员函数比友元函数参数少一个。

#include <iostream>

using namespace std;

class MyInt
{
private:
    int a;
public:
    MyInt(int a):a(a)
    {
//        cout << "构造函数" << endl;
    }

    int get_int()
    {
        return a;
    }
    // +运算符重载 友元函数实现
    
    MyInt operator +(MyInt &i2);
    
    MyInt operator ++(); // 前置++
    MyInt operator ++(int); // 后置++
};

MyInt MyInt::operator +(MyInt &i2)
{
    // int → MyInt 隐式调用构造函数
    return this->a + i2.a;
}

MyInt MyInt::operator ++()
{
    return ++this->a;
}

MyInt MyInt::operator ++(int)
{
    return this->a++;
}

int main()
{
    MyInt a1(1);
    MyInt a2(2);

    MyInt a3 = a1 + a2; // a1.op+(a2)
    cout << (++a3).get_int() << endl;  // 前置自增 4
    cout << (a3++).get_int() << endl;  // 4
    cout << (a3++).get_int() << endl;  // 5
    cout << a3.get_int() << endl;      // 6
    return 0;
}

2.4 特殊运算符重载

2.4.1 赋值运算符重载

除了之前学的无参构造函数、拷贝构造函数与析构函数以外,如果程序员不手写,编译器还会给一个类添加赋值运算符重载。

赋值运算符重载只能使用成员函数运算符重载。

#include <iostream>

using namespace std;

class MyInt
{
private:
    int a;
public:
    MyInt(int a):a(a)
    {
//        cout << "构造函数" << endl;
    }

    int get_int()
    {
        return a;
    }
    // +运算符重载 友元函数实现
    MyInt operator +(MyInt &i2);

    MyInt operator ++(); // 前置++
    MyInt operator ++(int); // 后置++

    // 编译器会自动添加赋值运算符重载函数
    MyInt & operator =(const MyInt &i)
    {
        cout << "赋值运算符重载函数被调用了" << endl;
        this->a = i.a;
        return *this;
    }
};

MyInt MyInt::operator +(MyInt &i2)
{
    // int → MyInt 隐式调用构造函数
    return this->a + i2.a;
}

MyInt MyInt::operator ++()
{
    return ++this->a;
}

MyInt MyInt::operator ++(int)
{
    return this->a++;
}

int main()
{
    MyInt a1(1);
    MyInt a2(2);

    MyInt a3 = a1 + a2;
    cout << (++a3).get_int() << endl;  // 前置自增 4
    cout << (a3++).get_int() << endl;  // 4
    cout << (a3++).get_int() << endl;  // 5
    cout << a3.get_int() << endl;      // 6
    MyInt a4 = a3;  // 调用拷贝构造函数
    a4 = a2;        // 赋值运算符重载
    a3 = a2;
    cout << a3.get_int() << endl;

    return 0;
}

当类中出现指针类型的成员变量时,默认的赋值运算符重载函数类似于默认的浅拷贝构造函数,因此也需要手动辨析解决“浅拷贝”的问题。

【面试题】一个类什么也不写,编译器加了那些代码?

无参构造函数、拷贝构造函数、析构函数、赋值运算符重载函数。

空类的大小是一个字节。

如果不写任何权限,默认权限为私有权限。

2.4.2 类型转换运算符重载

必须使用成员函数运算符重载,且格式比较特殊。

#include <iostream>

using namespace std;

class MyInt
{
private:
    int a;
    string str = "hello";
public:
    MyInt(int a):a(a)
    {
//        cout << "构造函数" << endl;
    }

    int get_int()
    {
        return a;
    }

    // 编译器会自动添加赋值运算符重载函数
    MyInt &operator =(const MyInt &i)
    {
        cout << "赋值运算符重载函数被调用了" << endl;
        this->a = i.a;
        return *this;
    }
    operator int()
    {
        return a;
    }

    operator string()
    {
        return str;
    }
};


int main()
{
    MyInt int1(2);
    int a1 = int1;
    cout << a1 << endl;

    string str2 = int1;
    cout << str2 << endl;

    return 0;
}

2.5 注意事项

  • 重载的运算符限制在C++语言中已有的运算符范围,不能创建新的运算符。
  • 运算符重载本质上也是函数重载,但是不支持函数参数默认值的设定。
  • 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符的操作数和语法结构。
  • 运算符重载必须基于或包含自定义类型,即不能改变基本类型的运算规则。
  • 重载的功能应该与原有功能类似,避免没有目的的滥用运算符重载。
  • 一般情况下,双目运算符建议使用友元函数进行重载,单目运算符建议使用成员函数运算符重载。

3.std::string 字符串类(熟悉)

字符串对象是一种特殊类型的容器,专门设计用于操作字符串。

#include <iostream>
#include <string.h>

using namespace std;

int main()
{
    string s;   // 创建一个空字符串
    // 判断是否为空
    cout << s.empty() << endl;

    // 调用隐式构造函数
    string s1 = "hello";
    cout << s1 << endl;

    // 显式调用构造
    string s2("world");
    cout << s2 << endl;

    // 判断编码 ==、!= 、> <
    cout << (s1 == s2) << endl; // 0
    cout << (s1 != s2) << endl; // 1
    cout << (s1 > s2) << endl;  // 0
    cout << (s1 < s2) << endl;  // 1

    // 拷贝构造函数
    string s3(s2);  // 等同于 string s3 = s2;
    cout << s3 << endl; // world

    // 参数1:char * 源字符串
    // 参数2:保留的字符数
    string s4("ABCDEFG",3);
    cout << s4 << endl; // ABC

    // 参数1:std::string 源字符串
    // 参数2:不保留的字符数
    string s5(s2,3);
    cout << s5 << endl; // ld

    // 参数1:字符数量
    // 参数2:字符内容 char
    string s6(5,'a');
    cout << s6 << endl; // aaaaa


    // 交换
    cout << "原s5=" << s5 <<" " << "原s6=" << s6 << endl;
    swap(s5,s6);
    cout << "s5=" << s5 <<" " << "s6=" << s6 << endl;

    // 字符串连接
    string s7 = s5 + s6;
    cout << s7 << endl; // aaaaald

    // 向后追加字符串
    s7.append("jiajia");
    cout << s7 << endl; // aaaaaldjiajia

    // 向后追加单字符
    s7.push_back('s');
    cout << s7 << endl; // aaaaaldjiajias

    // 插入
    // 参数1:插入的位置
    // 参数2:插入的内容
    s7.insert(1,"234");
    cout << s7 << endl; // a234aaaaldjiajias

    // 删除字符串
    // 参数1:起始位置
    // 参数2:删除的字符数量
    s7.erase(2,5);
    cout << s7 << endl; // a2aldjiajias

    // 替换
    // 参数1:起始位置
    // 参数2:被替换的字符数
    // 参数3:替换的新内容
    s7.replace(0,3,"*********");
    cout << s7 << endl; // *********ldjiajias

    // 清空
    s7.clear();
    cout << s7.length() << endl;    // 0

    // 直接赋值初始化(隐式调用构造函数)
    string s8 = "hahaha";
    cout << s8 << endl;

    // 重新赋值
    s8 = "ABCDEFGH";
    cout << s8 << endl; // ABCDEFGH

    // C++的string到c的string也就是数组
    // 参数1:拷贝的目标
    // 参数2:拷贝的字符数量
    // 参数3:拷贝的起始位置
    char arr[20] = {0};
    s8.copy(arr,6,1);
    cout << arr << endl;    // BCDEFG


    // C++ string 到C string用到了c语言的strcpy
    // c_str C++的字符串转换成C语言的字符数组
    // c_str返回一个const char *
    char c[20] ={0};
    strcpy(c,s8.c_str());
    cout << c << endl;      // ABCDEFGH

    return 0;
}

五、模板与容器

1.模板

模板可以让类或者函数支持一种通用类型,这种通用类型在实际运行的过程中可以使用任何数据类型,因此程序员可以写出一些与类型无关的代码,这种编程方式也被称为“泛型编程”。

通常有两种形式:

  • 函数模板
  • 类模板

1.1 函数模板(掌握)

使一个函数支持模板编程,可以使函数支持通用数据类型。

#include <iostream>

using namespace std;

template<typename T>    // class在当前学习两种关键字效果一样
T add(T a,T b)
{
    return a+b;
}

int main()
{
    string a = "hello";
    string b = "world";
    cout << add(a,b) << endl;
    // 如果T类型是自定义类型,则需要重载+运算符
    return 0;
}

1.2 类模板(掌握)

使一个类支持模板编程,可以使一个类支持通用数据类型。

#include <iostream>


using namespace std;

template<class T>
class Test
{
private:
    T val;
public:
    Test(T v):val(v){}

    T get_val()const
    {
        return val;
    }

    void set_val(const T& val)
    {
        this->val = val;
    }
};

int main()
{
//    Test t1(20);
//    cout << t1.get_val() << endl;

    Test<int> t1(20);
    cout << t1.get_val() << endl;

    Test<double>t2(23);
    cout << t2.get_val() << endl;

    Test<string>t3("hello");
    cout << t3.get_val() << endl;
    return 0;
}

类内声明类外实现

#include <iostream>


using namespace std;

template<class T>
class Test
{
private:
    T val;
public:
    Test(T v);

    T get_val()const;

    void set_val(const T& val);
};

template<class T>
Test<T>::Test(T v):val(v)
{

}

template<class T>
T Test<T>::get_val()const
{
    return val;
}

template<class T>
void Test<T>::set_val(const T& val)
{
    this->val = val;
}

int main()
{
//    Test t1(20);
//    cout << t1.get_val() << endl;

    Test<int> t1(20);
    cout << t1.get_val() << endl;

    Test<double>t2(23);
    cout << t2.get_val() << endl;

    Test<string>t3("hello");
    cout << t3.get_val() << endl;
    return 0;
}

2、容器

2.1 标准模板库STL

标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。虽说它主要出现到了C++中,但是在被引入C++之前该技术就已经存在了很长时间。

STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。

2.2 概念

容器是用来存储数据的集合,数据元素可以是任何类型。(因为是使用模板实现)

容器类的使用,都需要引入对应的头文件。

2.3 顺序容器

顺序容器中每个元素均有固定的位置并呈现线性排布,除非使用删除或者是插入的操作改变元素位置。

2.3.1 array数组(熟悉)

array是C++11新增的容器类型,与传统数组相比更加安全、更加易于使用。array数组是定长的,没有办法方便的伸缩。

#include <iostream>
#include <array>

using namespace std;

int main()
{
    // 创建一个长度为5的int数组
    array<int,5>arr = {1,2,3}; // 后面两位补零
    cout << arr[1] << endl;
    cout << arr[4] << endl;

    arr[3] = 200;
    cout << arr.at(3) << endl;
    // for 循环遍历
    for(int i = 0; i < arr.size(); i++)
    {
        cout << arr.at(i) << " ";
    }
    cout << endl;

    for(int i:arr)
    {
        cout << i <<" ";
    }
    cout << endl;
    return 0;
}

2.3.2 vector向量(掌握)

vector内部是由数组实现的,比较适合进行随机存取操作,不擅长删除插入操作。

#include <iostream>
#include <vector>

using namespace std;

int main()
{
//    vector<int> v = {1,2,3};
//    for(int i:v)
//    {
//        cout << i << endl;
//    }

    // 创建一个长度为5的向量(int)
    vector<int> vec(5);
    cout << vec.size() << endl;

    // 增
    vec.push_back(222); // 向后追加元素
    cout << vec.size() << endl; // 6

    // 插入操作
    vec.insert(vec.begin()+2,333); // begin()可以返回一个指向第一个元素的迭代器指针,+2是在第三个位置上插入333

    // 改
    vec[0] = 1;
    vec.at(1) = 2;
    vec.at(3) = 4;
    vec.at(4) = 5;

    // 删
    // 删除最后一个元素
    vec.pop_back();
    vec.erase(vec.begin()+1);   // 删除第二个元素
    vec.erase(vec.end()-2); // 删除倒数第二个元素


    // 查
    cout << vec[1] << endl;
    cout << vec.at(0) << endl;

    // for遍历
    for(int i = 0; i < vec.size(); i++)
    {
        cout << vec[i]<< " ";
    }
    cout << endl;

    for(int i:vec)
    {
        cout <<i <<" ";
    }
    cout << endl;
    // 判断是否为空,0非空,1空
    cout << vec.empty() << endl;

    // 清空
    vec.clear();
    cout << vec.empty() << endl;
    cout << vec.size() << endl;
    // 迭代器遍历

    return 0;
}

2.3.3 list列表(掌握)

list内部是由双向循环链表实现的。内存空间不连续,不支持下标。优点:可以高效的插入和删除操作。不适合随机存取。

#include <iostream>
#include <list>

using namespace std;

int main()
{
    // 创建一个默认无数值的list
//    list<string> lis1;

    // 创建一个长度为2的列表,第一个元素为hello第二个元素为world
//    list<string> lis2{"hello","world"};
//    for(string s:lis2)
//    {
//        cout << s << endl;
//    }

    // 创建一个长度为5的列表,每个元素都是“hello”
    list<string> lis(5,"hello");

    // 增
    lis.push_back("world"); // 向后追加单元素
    lis.push_front("hahaha"); // 向前追加单元素

    lis.insert(++lis.begin(),"222");    // 在第二个位置上插入“222”

    // 删
//    lis.pop_back(); //删除最后一个元素
    lis.pop_front();    // 删除第一个元素


    // 保存迭代器指针
    list<string>::iterator iter = lis.begin();
    advance(iter,1);    // 移动迭代器指针
    lis.insert(iter,"333");     // 插入333

//    iter = lis.end();
//    iter--;
//    lis.erase(iter);

    iter = lis.begin();
    advance(iter,1);
    lis.erase(iter);

    //  第一个元素的引用
//    cout << lis.front() << endl;
    // 返回最后一个元素的引用
//    cout << lis.back() << endl;

    // 改
    iter = lis.end();
    advance(iter,2);
    *iter = "200";

    cout << "----" << *iter << endl;

    // 不能用普通循环遍历,因为不支持下标

    for(string s:lis)
    {
        cout << s << endl;
    }


    // 也支持迭代器遍历

    // 清空
    lis.clear();

    cout << lis.size() << endl;
    return 0;
}

2.3.4 deque队列(熟悉)

deque支持几乎所有vector的API,性能位于vector与list两者之间。最擅长两端存取的顺序容器。

#include <iostream>
#include <deque>

using namespace std;

int main()
{
//    deque<int> v = {1,2,3};
//    for(int i:v)
//    {
//        cout << i << endl;
//    }

    // 创建一个长度为5的向量(int)
    deque<int> deq(5);
    cout << deq.size() << endl;

    // 增
    deq.push_back(222); // 向后追加元素
    cout << deq.size() << endl; // 6

    // 插入操作
    deq.insert(deq.begin()+2,333); // begin()可以返回一个指向第一个元素的迭代器指针,+2是在第三个位置上插入333

    // 改
    deq[0] = 1;
    deq.at(1) = 2;
    deq.at(3) = 4;
    deq.at(4) = 5;是用作限定符

    // 删
    // 删除最后一个元素
    deq.pop_back();
    deq.erase(deq.begin()+1);   // 删除第二个元素
    deq.erase(deq.end()-2); // 删除倒数第二个元素


    // 查
    cout << deq[1] << endl;
    cout << deq.at(0) << endl;

    // for遍历
    for(int i = 0; i < deq.size(); i++)
    {
        cout << deq[i]<< " ";
    }
    cout << endl;

    for(int i:deq)
    {
        cout <<i <<" ";
    }
    cout << endl;
    // 判断是否为空,0非空,1空
    cout << deq.empty() << endl;

    // 清空
    deq.clear();
    cout << deq.empty() << endl;
    cout << deq.size() << endl;
    // 迭代器遍历
    return 0;
}

2.4 关联容器(掌握)

关联容器的各个元素之间没有严格的顺序,虽然内部具有排序的特点,但是在使用时没有任何顺序相关的接口。

最常用的关联容器就是map-键值对映射。

对于map而言,键具有唯一性,键通常使用字符串类型,值可能是任何类型。通过键找到对应的值。

#include <iostream>
#include <map>

using namespace std;

int main()
{
    // 列表初始化创建C++11支持
    map<string,int>ma1 = {{"身高",190},{"体重",250}};
    cout << ma1.size() << endl; // 2

    map<string,int>ma;
    cout << ma.size() << endl;

    // 增
    ma["身高"] = 180; // 插入元素
    cout << ma.size() << endl;
    ma.insert(pair<string,int>("体重",70));   // 插入元素
    cout << ma.size() << endl;

    // 改
    ma["身高"] = 175;

    // 查
    cout << ma["身高"] << endl;   // 输出元素
    cout << ma["体重"] << endl;

    if(ma.find("身高") == ma.end())   // find从头开始查找,如果没有找到返回end
    {
        cout << "没有查找到身高元素" << endl;
    }
    else
    {
        cout << ma["身高"] << endl;
    }

    // 删
    int ret = ma.erase("身高"); // 返回值1删除成功,0失败
    cout << ret << endl;

    ret = ma.erase("月薪") ;
    cout << ret << endl;
    cout << ma.size() << endl;

    ma.clear();
    cout << ma.size() << endl;

    return 0;
}

2.5 迭代器遍历

迭代器是一个特殊的指针,主要用于容器的元素读写以及遍历。

如果迭代器不进行修改操作,建议使用只读迭代器,const_iterator。反之使用iterator。

#include <iostream>
#include <map>
#include <list>
#include <deque>
#include <vector>
#include <array>

using namespace std;

int main()
{
    string s = "abcdefg";
    // 迭代器遍历string
    for(string::const_iterator iter = s.begin(); iter != s.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
    cout << "-----------------" << endl;

    // 迭代器遍历array
    array<int,5> arr= {23,54,1,34,6};
    for(array<int,5>::const_iterator iter = arr.begin(); iter != arr.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
    cout << "-----------------" << endl;


    // 迭代器遍历vector
    vector<string> vec(6,"hello");
    for(vector<string>::const_iterator iter = vec.begin(); iter != vec.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
    cout << "-----------------" << endl;

    // 迭代器遍历list
    list<string>lis(6,"world");
    for(list<string>::const_iterator iter = lis.begin();iter != lis.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
    cout << "-----------------" << endl;

    // 迭代器遍历deque
    deque<string>deq(6,"hahaha");
    for(deque<string>::const_iterator iter = deq.begin(); iter != deq.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
    cout << "-----------------" << endl;

    // 迭代器遍历map
    map<string,int>ma;
    ma["年龄"] = 100;
    ma["身高"] = 190;
    ma["体重"] = 170;
    ma["薪资"] = 50000;

    for(map<string,int>::const_iterator iter = ma.begin();iter != ma.end(); iter++)
    {
        // first 是键 second 是值
        cout << iter->first << " " << iter->second  << endl;
    }
    cout << endl;
    cout << "-----------------" << endl;

    return 0;
}

六、面向对象核心

1.继承(重点)

 1.1 概念

继承是面向对象的三大特性之一,体现了代码复用的思想。

继承就是在一个已存在的类的基础上,创建一个新的类,并拥有其特性。

  • 已存在的类被称为“基类”或者“父类”
  • 新建立的类被称为“派生类”或“子类”
#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};

// 派生类(派生类继承基类Father)
class Son:public Father
{

};


int main()
{
//    Father f1;
//    cout << f1.get_name() << endl;
//    f1.work();

    Son son;
    cout << son.get_name() << endl;
    son.work();

    return 0;
}

上面的代码,Son类的功能几乎与Father类重叠,在实际的使用过程中,派生类会做出一些与基类的差异化。

  • 修改基类下来基类内容

属性:1、公有属性,直接更改。2、私有属性派生类无法直接进行修改和访问。如果需要使用或者更改基类的私有属性,需要使用基类公有函数进行修改。

函数:函数隐藏。通过派生类实现一个同名同参数的函数,来隐藏基类的函数。

  • 新增派生类的内容
#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    int age = 12;
    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};

// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
    void init()
    {
        age = 10;
        set_name("王");
    }

    void game()
    {
        cout << "我不光干活,我还打游戏,王者荣耀启动" << endl;
    }

    void work()
    {
        cout << "我是一个程序员,我在敲代码" << endl;
    }
};


int main()
{
    Son son;
    son.init();
    cout << son.get_name() << endl; // 王
    cout << son.age << endl; // 10
    son.work(); // 我是一个程序员,我在敲代码
    son.game(); // 我不光干活,我还打游戏,王者荣耀启动

    son.Father::work(); // 调用基类被隐藏的成员函数

    return 0;
}

基类与派生类是相对的,一个类可能存在又是基类又是派生类的情况,取决于那两个类进行比较。

1.2 构造函数(重点)

1.2.1 派生类与基类构造函数的关系

构造函数与析构函数不能被继承。

#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    // 有参构造函数
    Father(string name):name(name){}

    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};

// 派生类(派生类继承基类Father)
class Son:public Father
{
public:

};


int main()
{
    // 找不到基类的无参构造函数
//    Son son
//    Son son("张"); // 找不到派生类有参构造函数


    return 0;
}

派生类的任意构造函数,都必须直接或者间接调用基类的任意一个构造函数。

1.2.2 解决方案

1.2.2.1 补充基类的无参构造函数

#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    Father()
    {

    }
    // 有参构造函数
    Father(string name):name(name){}

    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};

// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
};


int main()
{
//    Son son;
    Son son; 
    return 0;
}

1.2.2.2 手动在派生中调用基类构造函数
1.2.2.2.1 透传构造

在派生类的构造函数中,调用基类的构造函数,实际上编译器自动添加的派生类的构造函数,调用基类构造函数时,采用的就是这种方式。

#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    // 有参构造函数
    Father(string name):name(name){}

    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};


// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
    // 透传构造
    Son():Father("张"){}

    // 手动添加派生类有参构造函数
    Son(string fn):Father(fn){}
};


int main()
{
//    Son son;
//    cout << son.get_name() << endl;

    Son son("王");
    cout << son.get_name() << endl;

    return 0;
}

1.2.2.2.2 委托构造

一个类的构造函数可以调用这个类的另一个构造函数,但是要避免循环委托。

委托构造的性能低于透传构造,但是代码的“维护性更好”,因为通常一个类中构造函数都会委托给能力最强(参数最多)的构造函数,代码重构时,只需要更改这个能力最强的构造函数即可。

#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    // 有参构造函数
    
    Father(string name):name(name){}

    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};


// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
    // 委托构造
    Son():Son("张"){}
    // 手动添加派生类有参构造函数
    Son(string fn):Father(fn){}
};


int main()
{
//    Son son;
//    cout << son.get_name() << endl;

    Son son;
    cout << son.get_name() << endl;

    return 0;
}

1.2.2.2.3 继承构造

C++11新增的写法,只需要一句话,就可以自动给派生类添加n(n为基类构造函数的个数)个构造函数。并且每个派生类的构造函数格式都与基类相同,每个派生类的构造函数都通过透传构造调用对应格式的基类构造函数。

#include <iostream>

using namespace std;

// 基类
class Father
{
private:
    string name = "孙";
public:
    Father():Father("张"){}  // 委托构造
    // 有参构造函数
    
    Father(string name):name(name){}

    void set_name(string name)
    {
        this->name = name;
    }

    string get_name()
    {
        return name;
    }

    void work()
    {
        cout << "我的工作是农民,我负责种地" << endl;
    }
};


// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
    // 只加这一句话,编译器就会自动添加下面两种构造函数
    using Father::Father;
//    Son():Father(){}
//    // 手动添加派生类有参构造函数
    
//    Son(string fn):Father(fn){}
};

int main()
{
//    Son son;
//    cout << son.get_name() << endl;

    Son son("王");
    cout << son.get_name() << endl;

    return 0;
}

1.3 对象的创建与销毁流程(掌握)

在继承中,构造函数与析构函数的调用顺序。

#include <iostream>

using namespace std;

class Value
{
private:
    string str;
public:
    Value(string str):str(str)
    {
        cout << str <<"构造函数" << endl;
    }

    ~Value()
    {
        cout << str << "析构函数" << endl;
    }
};

class Father
{
public:
    static Value s_value;
    Value val = Value("Father成员变量"); // 相当于 int i = int(10);
    Father()
    {
        cout << "Father构造函数调用了" << endl;
    }

    ~Father()
    {
        cout << "Father析构函数调用了" << endl;
    }
};
Value Father::s_value = Value("静态FatherValue被创建了");


class Son:public Father
{
public:
    static Value s_value;
    Value val = Value("son成员变量");
    Son()
    {
        cout << "Son 构造函数被调用了" << endl;
    }

    ~Son()
    {
        cout << "Son 析构函数被调用了" << endl;
    }
};
Value Son::s_value = Value("静态SonValue被创建了");

int main()
{
    cout << "主函数被调用了" << endl;
    {   // 局部代码块
        Son s;
        cout << "对象执行中" << endl;
    }
    cout << "主函数结束了" << endl;
    return 0;
}

上面的执行结果中,可以得到以下规律:

  • 静态的创建早于非静态。
  • 变量早于函数执行。
  • 在创建的过程中,同类型的内存区域基类先开辟。析构时派生类早于基类。
  • 以“对象执行中为轴”,上下对称。

1.4 多重继承(熟悉)

1.4.1 概念

C++支持多重继承,即一个派生类可以有多个基类。派生类对于每个基类的关系仍然可以看作是一个单继承。

#include <iostream>

using namespace std;

class Sofa
{
public:
    void sit()
    {
        cout << "沙发可以坐着" << endl;
    }
};

class Bed
{
public:
    void lay()
    {
        cout << "床可以躺着" << endl;
    }
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};

int main()
{
    SofaBed sb;
    sb.lay();
    sb.sit();
    return 0;
}

1.4.2 可能出现的问题

1.4.2.1 问题1-重名问题

当多个基类具有重名成员时,编译器在编译的过程中会出现二义性的问题。

#include <iostream>

using namespace std;

class Sofa
{
public:
    void sit()
    {
        cout << "沙发可以坐着" << endl;
    }

    void clean()
    {
        cout << "打扫沙发" << endl;
    }
};

class Bed
{
public:
    void lay()
    {
        cout << "床可以躺着" << endl;
    }

    void clean()
    {
        cout << "打扫床" << endl;
    }
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};

int main()
{
    SofaBed sb;
    sb.lay();
    sb.sit();
    sb.Bed::clean();
    sb.Sofa::clean();
    return 0;
}


1.4.2.2 问题2-菱形继承(熟悉)

当一个派生类有多个基类,且这些基类又有一个共同基类时,就会出现二义性问题,这种现象也被称为菱形(钻石)继承。

有两种解决方式:

  1. 使用基类的类名::方式调用。
#include <iostream>

using namespace std;

// 家具厂
class Furniture
{
public:
    void func()
    {
        cout << "家具厂里有家具" << endl;
    }
};


class Sofa:public Furniture
{
public:
};

class Bed:public Furniture
{
public:
};

// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};

int main()
{
    SofaBed sb;
    sb.Sofa::func();
    sb.Bed::func();
    return 0;
}


2、使用虚继承

当出现虚继承时,Furniture类中会产生一张虚基类表,这个表不占用任何对象的存储空间,属于Furniture类持有,在程序启动时加载进内存,表中记录了Furniture函数的调用地址偏移量。

Bed和Sofa对象会出现一个隐藏的成员变量指针,指向Furniture类中的虚基类表,占用对象4个字节。

虚继承(SofaBed继承Sofa和Bed)时,SofaBed类对象会同时持有两个虚基类表指针成员,在调用时查表解决二义性问题。

#include <iostream>

using namespace std;

// 家具厂
class Furniture
{
public:
    void func()
    {
        cout << "家具厂里有家具" << endl;
    }
};


class Sofa:virtual public Furniture
{
public:
};

class Bed:virtual public Furniture
{
public:
};

// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};

int main()
{
    SofaBed sb;
    sb.func();

    return 0;
}

2、权限(掌握)

2.1 权限修饰符

三种权限一共对应九种场景。要求心中有表,遇到每种场景都能直接反映出是否能访问。

类内

派生类中

全局

private

×

×

protected

×

public

#include <iostream>

using namespace std;

class Base
{
// 最小权限法则。首先推荐用私有,其次保护、最后公有
protected:
    string s = "保护权限";
public:
    Base()
    {
        cout << s << endl;
    }
};

class Son:public Base
{
public:
    Son()
    {
        cout << s << endl;
    }
};

int main()
{
    Son s1;
//    cout << s1.s << endl; // 错误s是保护权限,类外无法访问
    return 0;
}

2.2 不同权限的继承(掌握)

2.2.1 公有继承

上面的代码中一直使用的就是公有继承,公有继承也是使用最多的一种继承方式。

在公有继承中,派生类可以继承基类的成员,不可以访问基类的私有成员,基类的公有成员与保护成员、私有成员在派生类中权限不变。

#include <iostream>

using namespace std;

class Base
{
private:
    string str1 = "私有成员";
protected:
    string str2 = "保护成员";
public:
    string str3 = "公有成员";
};

class Son:public Base   // 公有继承
{
public:
    Son()
    {
//        cout << str1 << endl; // 错误 str1为私有成员
        cout << str2 << endl;
        cout << str3 << endl;
    }
};

int main()
{
    Son s1;
//    cout << s1.str1 << endl;   // 错误私有成员
//    cout << s1.str2 << endl;   // 错误保护成员
    cout << s1.str3 << endl;
    return 0;
}

2.2.2 保护继承

在保护继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员在派生类的权限都是保护权限。(只能在基类与派生类中访问,外部无法访问)。

#include <iostream>

using namespace std;

class Base
{
private:
    string str1 = "私有成员";
protected:
    string str2 = "保护成员";
public:
    string str3 = "公有成员";
};

class Son:protected Base   // 保护继承
{
public:
    Son()
    {
//        cout << str1 << endl; // 错误 str1为私有成员
        cout << str2 << endl;
        cout << str3 << endl;
    }
};

int main()
{
    Son s1;
//    cout << s1.str1 << endl;   // 错误私有成员
//    cout << s1.str2 << endl;   // 错误保护成员
//    cout << s1.str3 << endl;   // 错误保护成员
    return 0;
}

2.2.3 私有继承

在私有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员与保护成员在派生类中都是私有权限。

#include <iostream>

using namespace std;

class Base
{
private:
    string str1 = "私有成员";
protected:
    string str2 = "保护成员";
public:
    string str3 = "公有成员";
};

class Son:private Base   // 私有继承
{
public:
    Son()
    {
//        cout << str1 << endl; // 错误 str1为私有成员
        cout << str2 << endl;
        cout << str3 << endl;
    }
};

class Test:public Son
{
public:
    Test()
    {
//        cout << str1 << endl; // 错误 
//        cout << str2 << endl;
//        cout << str3 << endl;
    }

};

int main()
{
    Son s1;
//    cout << s1.str1 << endl;   // 错误私有成员
//    cout << s1.str2 << endl;   // 错误私有成员
//    cout << s1.str3 << endl;   // 错误私有成员
    return 0;
}

3、多态(重点)

3.1 什么是多态

在面向对象编程中,我们通常将多态分为两种:静态多态(也称为编译时多态)和动态多态(称为运行时多态)两种多态性是多态概念的不同表现方式。

静态多态

  • 静态多态是指在编译时就能确定要调用的函数,通过函数重载和运算符重载来实现。

动态多态

  • 动态多态是指在运行时根据对象的实际类型来确定调用的函数,通过继承和函数覆盖来实现。

静态多态发生在编译时,因为在编译阶段编辑器就可以确定要调用的函数。

动态多态发生在运行时,因为具体调用那个函数是在程序运行时根据对象的实际类型来确定。

注:本文后续说的多态均为动态多态。

3.2 多态的概念

多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。

多态的使用具有三个前提条件:

  • 公有继承
  • 函数覆盖
  • 基类的指针/引用指向派生类的对象

多态的优点:多态会让你的代码更加灵活、更加具有可拓展性、可维护性。它能使代码更具有通用性,减少重复代码的编写。

多态的缺点:多态的缺点包括代码的复杂性、不易读、运行效率低、在运行时会产生一些额外的开销。

3.3 函数覆盖

函数覆盖、函数隐藏、这两个比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必备条件,函数覆盖比函数隐藏有以下几点区别:

  • 函数隐藏是指派生类中存在与基类同名同参的函数,编译器会将基类的同名同参的函数进行隐藏。
  • 函数覆盖是基类中定义一个虚函数,派生类编写一个与基类同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的函数必须是虚函数。

3.4 虚函数的定义

一个函数使用virtual关键字修饰,就是虚函数。虚函数是函数覆盖的前提。在QtCretor中虚函数的函数名称使用斜体字。

#include <iostream>

using namespace std;

class Animal
{
public:
    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

int main()
{

    return 0;
}

虚函数具有以下性质:

  • 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
#include <iostream>

using namespace std;

class Animal
{
public:
    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()
    {
        cout << "狗爱吃骨头" << endl;
    }
};


int main()
{

    return 0;
}

  • 只有普通成员函数与析构函数可以声明为虚函数。
#include <iostream>

using namespace std;

class Animal
{
public:

    // 错误,构造函数不能声明为虚函数
//    virtual Animal()
//    {

//    }

    // 错误,静态函数不能为虚函数
//    virtual static void testStatic()
//    {
//        cout << "测试静态成员函数 虚函数" << endl;
//    }
    
    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()
    {
        cout << "狗爱吃骨头" << endl;
    }
};


int main()
{

    return 0;
}

  • 在C++11中,可以在派生类的新覆盖的函数上使用override关键字验证覆盖是否成功。
#include <iostream>

using namespace std;

class Animal
{
public:

    // 错误,构造函数不能声明为虚函数
//    virtual Animal()
//    {

//    }

    // 错误,静态函数不能为虚函数
//    virtual static void testStatic()
//    {
//        cout << "测试静态成员函数 虚函数" << endl;
//    }

    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }

    void funcHide()
    {
        cout << "测试 override关键字函数" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()override
    {
        cout << "狗爱吃骨头" << endl;
    }
    // 错误,标记覆盖但是没覆盖
    // 注:这个函数隐藏,并不是函数覆盖,因为基类中的函数是一个普通成员函数,不是虚函数
    void funcHide()override
    {
        cout << "测试 派生类override关键字函数" << endl;
    }
};


int main()
{

    return 0;
}

3.5 多态的实现

我们在开篇的时候提到过,要实现动态多态,需要具有三个前提条件。

  • 公有继承(已经实现)
  • 函数覆盖(已经实现)
  • 基类指针/引用指向派生类对象(还未编写)

【思考】为什么要基类的指针/引用指向派生类对象?

  • 实现运行时多态:当使用基类的指针或引用指向派生类对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或者引用的类型。
  • 统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象。这样可以使代码更加灵活,减少重复代码,并且能够更好支持代码的扩展性和维护性。
#include <iostream>

using namespace std;

class Animal
{
public:
    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()override
    {
        cout << "狗爱吃骨头" << endl;
    }
};


class Cat:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()override
    {
        cout << "猫爱吃鱼" << endl;
    }
};


int main()
{
    // 基类的指针指向派生类的对象
    Animal *a1 = new Dog;
    a1->eat();

    Animal *a2 = new Cat;
    a2->eat();



    return 0;
}


提供通用函数接口,参数设计成基类的指针或者引用,这样这个函数就可以访问到此基类所有派生类中的虚函数了。

#include <iostream>

using namespace std;

class Animal
{
public:
    // 虚函数
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()override
    {
        cout << "狗爱吃骨头" << endl;
    }
};


class Cat:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void eat()override
    {
        cout << "猫爱吃鱼" << endl;
    }
};
// 提供通用函数,形参为基类指针
void animal_eat1(Animal *a1)
{
    a1->eat();
}

// 提供通用函数,形参为基类引用
void animal_eat2(Animal &a1)
{
    a1.eat();
}


int main()
{
    // 基类的指针指向派生类的对象
//    Animal *a1 = new Dog;
//    a1->eat();

//    Animal *a2 = new Cat;
//    a2->eat();

    Dog *d1 = new Dog;
    Cat *c1 = new Cat;
    animal_eat1(d1);
    animal_eat1(c1);

    Dog d2;
    Cat c2;
    animal_eat2(d2);
    animal_eat2(c2);

    return 0;
}

3.6 多态原理

具有虚函数的类会存在一张虚函数表,每个对象内部都有一个隐藏的虚函数表指针成员,指向当前类的虚函数表。

多态的实现流程:

在代码运行时,通过对象的虚函数表指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。

3.7 虚析构函数

如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,只能触发基类的析构函数,如果在派生类中申请堆内存等资源,则会导致无法释放,出现内存泄漏的问题。

#include <iostream>

using namespace std;

class Animal
{
public:
    // 虚函数
//    virtual void eat()
//    {
//        cout << "动物爱吃饭" << endl;
//    }
    // 虚析构函数
    virtual ~Animal()
    {
        cout << "基类析构函数" << endl;
    }
};

class Dog:public Animal
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
//    void eat()override
//    {
//        cout << "狗爱吃骨头" << endl;
//    }

    ~Dog()
    {
        cout << "派生类析构函数" << endl;
    }
};



int main()
{
    // 基类的指针指向派生类的对象
    Animal *a1 = new Dog;
//    a1->eat();
    delete a1;


    return 0;
}

解决方案是给基类的析构函数使用virtual修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数,因此建议给一个可能为基类的类中的析构函数设置成虚析构函数。

3.8 类型转换(熟悉)

在上一节中除了虚析构函数外,还可以使用类型转换解决内存泄漏问题,以下是传统的类型转换写法:

#include <iostream>

using namespace std;

class Animal
{
public:
    ~Animal()
    {
        cout << "基类析构函数" << endl;
    }
};

class Dog:public Animal
{
public:
    ~Dog()
    {
        cout << "派生类析构函数" << endl;
    }
};


int main()
{
    Animal *a1 = new Dog;
    // 可以把a转换回Dog*类型
    Dog *d = (Dog*)a1;
    delete d;

    return 0;
}

在C++11中不建议使用C风格的类型转换,因此可能会带来一些安全隐患,让程序的错误难以发现。

C++11提供了一组适用于不同场景的强制类型转换函数。

  • static_cast(静态转换)
  • dynamic_cast(动态转换)
  • const_cast(常量转换)
  • reinterpret_cast(重解释转换)
3.8.1 static_cast
  • 主要用于基本数据类型之间的转换。
#include <iostream>

using namespace std;

int main()
{
    int x = 1;
    double y = static_cast<double>(x);
    cout << y << endl;
    return 0;
}

static_cast没有运行时类型检查来保证转换的安全性,需要程序员手动判断转化是否安全。

#include <iostream>

using namespace std;

int main()
{
    double x = 3.14;
    int y = static_cast<int>(x);
    cout << y << endl;
    return 0;
}

static_cast也可以用于类层次的转换中,即基类和派生类指针或引用之间的转换。

  • static_cast进行上行转换是安全的,即把派生类的指针或引用转为基类的。
  • static_cast进行下行转换是不安全的,即把基类的指针或引用转换为派生类的。

static仅仅可以完成上述转换,但是不建议。

#include <iostream>

using namespace std;


class Father
{
public:
    string a = "Father";
};

class Son:public Father
{
public:
    string b = "Son";
};

int main()
{
    // 指针转换
    // 上行转换 派生类→基类
//    Son *s1 = new Son;
//    Father *f1 = static_cast<Father*>(s1);
//    cout << f1->a << endl;  // Father

//    // 下行转换 基类→派生类
//    Father *f2 = new Father;
//    Son *s2 = static_cast<Son*>(f2);
//    cout << s2->a << endl;  // Father
//    cout << s2->b << endl;  // 结果不定


    // 引用转换 上行转换:派生类→基类
    Son s1;
    Father f1 = static_cast<Father>(s1);
    Father &f2 = static_cast<Father&>(s1);

    cout << f1.a << endl;
    cout << f2.a << endl;
    cout << &s1 << endl;    // 0x61fe84
    cout << &f1 << endl;    // 0x61fe80
    cout << &f2 << endl;    // 0x61fe84


    // 下行转换 基类→派生类
    Father f3;
//    Son s2 = static_cast<Son>(f3); // 错误
    Son &s3 = static_cast<Son&>(f3);
    cout << s3.a << endl;   // Father
    cout << s3.b << endl;   // Father

    return 0;
}

3.8.2 dynamic_cast

dynamic_cast主要用于类层次之间的上行与下行转换。

在进行上行转换时,dynamic_cast与static_cast效果相同,但是进行下行转换时,dynamic_cast会比static_cast更加安全。

关于下行转换的类型检查如下:

#include <iostream>
using namespace std;

class Father
{
public:
    virtual void func()
    {
        cout << "Father" << endl;
    }
};

class Son:public Father
{
public:
    void func()
    {
        cout << "Son" << endl;
    }

};

int main()
{
    // 指针且形成多态
    Father *f0 = new Son;
    Son *s0 = dynamic_cast<Son*>(f0);
    cout << f0 << " " << s0 << endl;
    f0->func();
    s0->func();

    // 指针未形成多态
    Father *f1 = new Father;
    Son* s1 = dynamic_cast<Son*>(f1);
    cout << f1 << " " << s1 << endl;    // 0xf62750 0
    f1->func(); // Father
//    s1->func(); // 非法调用


    // 引用且形成多态
    Son s;
    Father &f2 = s;
    Son &s2 = dynamic_cast<Son&>(f2);
    cout << &s2 << " " << &f2 << " " << &s << endl; // 0x61fe74 0x61fe74 0x61fe74
    s2.func();  // Son
    f2.func();  // Son
    s.func();   // Son

    Father f;
//    Son& s3 = dynamic_cast<Son&>(f); // 运行终止
    cout << &s3 << " " << &f << endl;
    s3.func();
    f.func();

    return 0;
}

3.8.3 const_cast

const_cast可以添加或者移除对象的const限定符。

主要用于改变指针或引用的const效果,以便于在一定的情况下修改原本被声明为常量的对象,应该避免使用const_cast,而是考虑通过良好的接口设计或者其他正常手段避免需要进行此种转换。

#include <iostream>
using namespace std;

class Test
{
public:
    string str = "A";
};


int main()
{
    const Test* t1 = new Test;
//    t1->str = "B"; // 错误
    Test *t2 = const_cast<Test*>(t1);
    t2->str = "B";
    cout << t1 << " " << t2 << endl;
    cout << t1->str << " " << t2->str << endl;

    return 0;
}

3.8.3 reinterpret_cast

reinterpret_cast 可以把内存里的值重新解释,这种转换风险极高,慎用!!!!!!

#include <iostream>
using namespace std;

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

class B
{
public:
    void print()
    {
        cout << "B" << endl;
    }
};


int main()
{
    A*a = new A;
    B *b = reinterpret_cast<B*>(a);
    cout << a << " " << b << endl;  // 0x1052740 0x1052740
    a->print(); // A
    b->print(); // B
    return 0;
}

4抽象类掌握)

如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类。

如果一类中有纯虚函数,则这个类是一个抽象类。

如果一类是抽象类,则这个类中一定有纯虚函数。

纯虚函数是虚函数的一种,这种函数只有声明没有定义。

virtual 返回值类型 函数名(参数列表)= 0;

不能直接使用抽象类作为声明类型,因为不存在抽象类类型的对象。(不能实例化对象)

抽象类作为基类时,具有两种情况:

  • 派生类继承抽象类,覆盖并实现其所有的纯虚函数,此时派生类可以作为普通类进行使用,即不再是抽象类。
  • 派生类继承抽象类,没有把抽象类中的所有纯虚函数覆盖并实现,此时派生类也变为抽象类,等待它的派生类覆盖并实现剩余的纯虚函数。
#include <iostream>
using namespace std;

// 抽象类::形状
class Shape
{
public:
    // 纯虚函数
    virtual void area() = 0;    // 面积
    virtual void perimeter() = 0;   // 周长
};

// 圆形
class Circle:public Shape
{
public:
    // 函数覆盖并实现所有纯虚函数
    void area()
    {
        cout << "圆形计算面积" << endl;
    }

    void perimeter()
    {
        cout << "圆形计算周长" << endl;
    }
};

// 多边形
class polygon:public Shape
{
public:
    void perimeter()
    {
        cout << "多边形计算周长" << endl;
    }
};

// 矩形
class Rectangle:public polygon
{
public:
    void area()
    {
        cout << "矩形计算面积" << endl;
    }

};

int main()
{
//    Shape s; // 错误抽象类无法实例化对象(形状类)
    Circle c; // 圆类
    c.area();
    c.perimeter();

//    polygon p;  // 错误 抽象类无法实例化对象

    Rectangle r;
    r.area();   // 矩形的面积
    r.perimeter();  // 多边形的周长

    return 0;
}


使用抽象类需要注意以下几点:

  • 抽象类的析构函数必须是虚析构。
  • 抽象类支持多态,可以存在引用或指针的声明格式。
  • 因为抽象类的作用是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。

5虚析构熟悉)

纯虚析构函数的定义:

纯虚析构函数的本质:是析构函数,作用是各个类的回收工作,而且析构函数不能被继承。

必须为纯虚析构函数提供一个函数体。

纯虚析构函数,必须在类外实现。

#include <iostream>
using namespace std;

class Animal
{
public:
    // 纯虚析构函数
    virtual ~Animal() = 0;
};

// 实现
Animal::~Animal()
{
    cout << "基类的析构函数被调用了" << endl;
}


class Dog:public Animal
{
public:
    ~Dog()
    {
        cout << "Dog析构函数" << endl;
    }
};

int main()
{
    Animal *a1 = new Dog;
    delete a1;

//    Animal a2;
    Dog d;
    return 0;
}

虚析构函数与纯虚析构函数的区别:

虚析构函数:virtual 关键字修饰,有函数体,不会导致基类为抽象类。

纯析构函数:virtual关键字修饰,结尾=0,函数体需要类外实现,会导致类为抽象类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2087344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 7.3 - 分布式 id

分布式 ID 介绍 什么是 ID&#xff1f; ID 就是 数据的唯一标识。 什么是分布式 ID&#xff1f; 分布式 ID 是 分布式系统中的 ID&#xff0c;它不存在于现实生活&#xff0c;只存在于分布式系统中。 分库分表&#xff1a; 一个项目&#xff0c;在上线初期使用的是单机 My…

2-80 基于matlab-GUI,实现kalman滤波对目标物的位置进行检测跟踪

基于matlab-GUI,实现kalman滤波对目标物的位置进行检测跟踪。检测汽车中心和最大半径&#xff0c;与背景差分选择较大差异的区域进行形态学处理&#xff0c;用冒泡法对目标面积从大到小排序。程序已调通&#xff0c;可直接运行。 2-80 kalman视频跟踪滤波 - 小红书 (xiaohongsh…

光学涡旋Talbot阵列照明器的matlab模拟与仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 光学涡旋 Talbot 阵列照明器是一种利用光学涡旋&#xff08;Optical Vortex&#xff09;和 Talbot 效应&#xff08;Talbot Effect&#xff09;相结合的技术&…

【HTML源码】上传即可使用的在线叫号系统源码

这个叫号系统的过程是这样的 接了一个任务&#xff0c;某学校要对学生进行逐个面试&#xff0c;希望能有类似医院门诊那种叫号系统。 条件&#xff1a;首先说硬件&#xff0c;就是教室里边一台笔记本电脑&#xff0c;同屏到教室外面的电视机。 需求&#xff1a;软件需求是可…

汉诺塔递归解决思路图解分析,python代码实现

目录 4.假设四层汉诺塔&#xff0c;n4&#xff0c;利用整体思想分解为两层的情况 3.分解到n3 3.1 分解上面n4时第一个步骤&#xff1a; 3.2 分解上面n4时第三个步骤&#xff1a; 2.继续分解到n2 &#xff08;同理略&#xff09; 1.当分解到n1 python代码 问题&#xff1…

【Linux】升级OpenSSH版本规避远程代码执行漏洞

本文首发于 ❄️慕雪的寒舍 升级OpenSSH版本规避远程代码执行漏洞。 说明 今天早上逛别人的博客的时候看到了这个重磅消息。OpenSSH爆出能远程通过root身份执行任意代码的漏洞&#xff0c;影响版本是 8.5p1 < OpenSSH < 9.8p1&#xff0c;奇安信的报告可以点我查看。 上…

计算机三级网络第4套练习记背

计算机三级网络第4套练习记背

全志/RK安卓屏一体机:医疗自助服务终端,支持鸿蒙国产化

医疗自助服务终端 为了解决传统医疗模式下的“看病难、看病慢”等问题&#xff0c;提高医疗品质、效率与效益&#xff0c;自助服务业务的推广成为智慧医疗领域实现信息化建设、高效运作的重要环节。 医疗自助服务终端是智慧医疗应用场景中最常见的智能设备之一&#xff0c;它通…

Linux学习笔记(4)----Debian压力测试方法

使用命令行终端压力测试需要两个实用工具&#xff1a;s-tui和stress sudo apt install s-tui stress 安装完成后&#xff0c;在终端中启动 s-tui实用工具&#xff1a; s-tui 执行后如下图&#xff1a; 你可以使用鼠标或键盘箭头键浏览菜单&#xff0c;然后点击“压力选项(Str…

day44.动态规划

718.最长重复子数组 给两个整数数组 nums1 和 nums2 &#xff0c;返回 两个数组中 公共的 、长度最长的子数组的长度 。 思路:1.确定dp数组&#xff08;dp table&#xff09;以及下标的含义: dp[i][j] &#xff1a;以下标i - 1为结尾的A&#xff0c;和以下标j - 1为结尾的B&…

【论文速读】|RO-SVD:一种用于 AIGC 应用的可重构硬件版权保护框架

本次分享论文&#xff1a;RO-SVD: A Reconfigurable Hardware Copyright Protection Framework for AIGC Applications 基本信息 原文作者&#xff1a;Zhuoheng Ran, Muhammad A.A. Abdelgawad, Zekai Zhang, Ray C.C. Cheung, Hong Yan 作者单位&#xff1a;香港城市大学电…

linux 内核代码学习(七)

linux内核代码的研究中断了一段时间了&#xff0c;现在又重新开始了研究&#xff0c;个人觉得linux内核的学习是没有上限的&#xff0c;总是一个温故而知新的过程&#xff0c;是一个不断积累的过程。首先还是要先搭建一个方便自己学习和研究的平台&#xff0c;经过不断的尝试&a…

Java的IO模型详解-BIO,NIO,AIO

一、BIO相关知识 Java 的 BIO (Blocking I/O) 模型是基于传统的同步阻塞 I/O 操作。在这种模型中&#xff0c;每个客户端连接都需要一个独立的线程来处理请求。当线程正在执行 I/O 操作时&#xff08;如读取或写入&#xff09;&#xff0c;它会被阻塞&#xff0c;直到操作完成…

三级_网络技术_55_应用题

一、 请根据下图所示网络结构回答下列问题。 1.填写RG的路由表项。 目的网络/掩码长度输出端口__________S0&#xff08;直接连接&#xff09;__________S1&#xff08;直接连接&#xff09;__________S0__________S1__________S0__________S1 (2)在不改变路由表项的前提下&…

django学习入门系列之第十点《案例 用户登录》

文章目录 案例 用户登录安全认证django中的隐藏值获得用户账户密码空值 往期回顾 案例 用户登录 安全认证 ​ 如果提交数据后&#xff0c;发现并没有跳转到自己想要的界面&#xff0c;是因为django比Flask多一层 ”安全机制“ 的东西 解决方法&#xff1a; {% csrf_token %…

使用maven搭建微服务框架

徒手搭建cloud 1.认准SpringBoot,SpringCloud,SpringCloudAlibaba版本之间的对用关系 官网给出了声明&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki 2.选择好版本之后 spring bootspring cloudspring cloud alibaba2.3.12.RELEASEHoxton.SR102.2.5.REL…

Ps:工具预设面板

Ps菜单&#xff1a;窗口/工具预设 Window/Tool Presets 工具预设 Tool Presets面板可以为 Photoshop 的图像编辑工作带来极大的便利。 定义好相关的工具预设后&#xff0c;可以直接调用&#xff0c;而不管现在处于什么工具或什么样的参数状态&#xff0c;省去了再次设置参数的麻…

使用 树莓派3B+ 对日本葡萄园进行经济实惠的环境监测

对于 菊岛邦夫—Vineyard Kikushima 而言&#xff0c;Raspberry Pi 生态系统提供了支持和信息&#xff0c;通过基于温度和湿度监测的有针对性的最低限度杀虫剂方案&#xff0c;来提高葡萄的健康产量。 Vineyard Kikushima&#xff1a;http://vykikushima.greater.jp/vineyards…

finalshell 用 root 账号连接 ubuntu

我们平时在操作 linux 系统时&#xff0c;经常需要上传文件&#xff0c;修改文件&#xff0c;普通账号只能通过 vim 等工具修改&#xff0c;诸多不便。为了实现跟 windows 一样&#xff0c;双击直接编辑保存&#xff0c;需要下面步骤。 1. ubuntu 安装 ssh 1.1 安装 SHH 服务…

LuaJit分析(十)luajit自定义修改

通过分析luajit字节码文件格式可知&#xff0c;luajit文件由文件头和原型数组组成&#xff0c;而原型又包括原型头和原型体&#xff0c;文件头中包含了字节码文件的一些关键信息&#xff0c;目前的反编译工具根据标准的luajit2.0文件格式解析文件&#xff0c;如果对字节码文件的…