C++中重要但又容易忘的知识点

(持续更新ing...)

1. 类和对象--静态成员和静态成员函数

 先看代码案例:

class People
{
public:
    static int m_Age;
    static void speak()
    {
        std::cout<<"this is a static function"<<endl;
    }
};
int People::m_Age = 100;

void test()
{
    People p;
    cout<<p.m_Age<<endl;
    p.speak();
    cout<<People.m_Age<<endl;
    People.speak();
}

知识点:

        静态成员变量需要在类内声明类外初始化,编译过程就分配内存,所以对象共享一份数据即可以通过类名直接调用该静态变量和函数而不属于某一具体对象。

2. 类和对象--菱形继承

 先看代码案例:

class Animal
{
public:
    Base()
    {
        m_Age = 30;
    }
    int m_Age;
};

class Sheep: virtual public Animal{};
class Tuo: virtual public Animal{};
class SheepTuo: public Sheep, public Tuo{};

void Test()
{
    SheepTuo st;
    st.Sheep::m_Age = 200;
    st.Tuo::m_Age = 100;
    cout<<st.Sheep::m_Age<<endl;
    cout<<st.Tuo::m_Age<<endl;
    cout<<st.m_Age<<endl;
}

知识点:

        先说上面代码的运行结果:三个输出都是同样的,等于100,因为此时SheepTuo只有一份m_Age数据。

        菱形继承类似于“爷爷--两个父亲--孙,子”这样一个形式,该继承类型出现一个问题:孙子类会继承两份相同的数据,导致资源浪费;

        引入虚继承和虚基类概念:在继承前加入virtual关键字属于虚继承,此时Animal属于虚基类。

        原理剖析:Sheep和Tuo通过虚继承操作,不会直接继承m_Age数据,而是分别继承了一个vbptr数据类型,即虚基类指针,而vbptr又分别指向一个vstable,即虚基类表,此时各自的vbtable中会记录一个数据,即地址偏移量,用于指向此时Animal中的m_Age。即最终SheepTuo中只继承了一份m_Age数据,解决了菱形继承的问题。

3. 类和对象--多态

 先看代码案例:

class Animal
{
public:
    virtual void speak()
    {
        cout<<"Animal is talking"<<endl;
    }
};

class Cat: public Animal
{
    void speak()
    {
        cout<<"Cat is talking"<<endl;
    }
};


class Dog: public Animal
{
    void speak()
    {
        cout<<"Dog is talking"<<endl;
    }
};

void doSpeak(Animal& animal) //C++允许父子之间的类型转换,无须强制类型转换
{
    animal.speak();
}

void test()
{
    Animal animal;
    Cat cat;
    Dog dog;
    doSpeak(animal);
    doSpeak(cat);
    doSpeak(dog);
}

知识点:

        先说上面代码的运行结果:三个输出都是不同的,等于各自成员函数的输出值。如果Animal类中的speak()函数无关键词virtural,则三个输出都是相同的,因为此时属于静态多态中的地址早绑定范畴。

        多态,顾名思义就是多种形态,引入黑马程序员的解释:

4d8793cb871a44d3930d1a1b8d1fa6a4.png

        上图重点:虚函数,即附带关键字virtual;动态多态的函数地址晚绑定,即函数地址在运行阶段才确定。

        多态的使用需满足条件:有继承关系;子类重写父类中的虚函数;父类指针或引用指向子类对象。

        原理剖析:当Animal类中通过virtural来创建一个虚函数时,会产生一个虚函数指针vfptr,该指针指向一个vftable虚函数表,该表记录虚函数的地址,即&Animal::speak();当创建一个Cat类时,对Animal类进行一个继承,即把父类的全部内容拿过来一份,也就包括vfptr和vftable,此时若Cat类不重写父类的虚函数的话,vftable中记录的仍然是&Animal::speak()这个虚函数地址,而当Cat类重写了父亲的虚函数之后,此时Cat继承过来的这个vfptr指向的vftable中记录的数据就会被替换为&Cat::speak()这个函数地址,即,此时通过doSpeak()函数运行animal.speak()函数的时候,就会开始去确定speak()函数的地址,此时传入的是cat对象,就会读取到其vftable中的地址进行调用了。

2024.10.12补充

        使用多态的时候需要注意虚析构函数和纯虚析构函数的使用。此情况适用于使用new关键词在栈区开辟数据需要手动释放的情况。由于虚函数的使用需要由父类指针接收子类对象进行使用,导致在delete父类在栈区的指针对象的时候,只会调用父类的析构函数而不会调用子类的析构函数,所以需要在父类中使用虚析构函数来解决这个问题。示例代码可以参考下面这个。

#include<iostream>
#include<string>
#include<cstddef>  //nullptr空指针需要用
using namespace std;


class waterBase
{
public:
    virtual void madeFunc() = 0;
    virtual ~waterBase() = 0;   //纯虚析构需要声明也需要实现,毕竟如果虚基类需要在栈区释放空间的话也得有具体实现代码。
};


waterBase::~waterBase()
{
    cout<<"waterBase的纯虚析构实现"<<endl;
}


class coffeeSon: public waterBase
{
public:

    coffeeSon(string Namen)
    {
        m_name = new string(Namen);
        cout<<"coffeeSon的构造函数"<<endl;
    }

    void madeFunc()
    {
        cout<<"I am making the coffee, it has four steps"<<endl;
    }
    string* m_name;

    ~coffeeSon()
    {
        cout<<"coffeeSon的析构函数"<<endl;
        if(m_name != nullptr)
        {
            delete m_name;
            m_name = nullptr;
        }
    }
};

void doWork(waterBase* base)
{
    base->madeFunc();
    delete base;
}

void test01()
{
    doWork(new coffeeSon("co"));
    //coffeeSon coffee("coffee");
    //waterBase *base = &coffee;
    //doWork(base);
    }

int main()
{
    test01();
    return 0;
}

4.不同的类数据想存在一个数组里面

        先看代码:

#include<iostream>
using namespace std;
#include<string>
#include<cstddef>

class worker
{
public:
    virtual ~worker() = 0;
    int ID;
    string Name;
    int partId;
    virtual void workDescribe() = 0;
    virtual string workTitle() = 0;
};
worker::~worker(){};

class workManager
{
public:
    int workerNum;
    worker ** workerArray;
    workManager()
    {
        this->workerNum = 0;
        this->workerArray = nullptr;
    };
    ~workManager(){};
    void showMenu();
    void Equit();
    void Add();
     
};



class ordinaryStaff:public worker
{
public:
    ordinaryStaff(int id, string name, int partid)
    {
        this->ID = id;
        this->Name = name;
        this->partId = partid;
    }
    void workDescribe()
    {
        cout<<"id: "<<this->ID
        <<"\tname: "<<this->Name
        <<"\tworkTitle: "<<this->workTitle()
        <<"\twork: "<<"Complete what the manager commanded..."<<endl;
    }
    string workTitle()
    {
        return "ordinaryStaff";
    }
};

class managerStaff:public worker
{
public:
    managerStaff(int id, string name, int partid)
    {
        this->ID = id;
        this->Name = name;
        this->partId = partid;
    }
    void workDescribe()
    {
        cout<<"id: "<<this->ID
        <<"\tname: "<<this->Name
        <<"\tworkTitle: "<<this->workTitle()
        <<"\twork: "<<"Complete what the boss commanded and command the ordinary staffs..."<<endl;
    }
    string workTitle()
    {
        return "managerStaff";
    }
};

class boss:public worker
{
public:
    boss(int id, string name, int partid)
    {
        this->ID = id;
        this->Name = name;
        this->partId = partid;
    }
    void workDescribe()
    {
        cout<<"id: "<<this->ID
        <<"\tname: "<<this->Name
        <<"\tworkTitle: "<<this->workTitle()
        <<"\twork: "<<"Running the company and command the manager staffs"<<endl;
    }
    string workTitle()
    {
        return "boss";
    }
};

void workManager::Equit()
{
    cout<<"Thanks for using..."<<endl;
    exit(0);  //可以直接退出程序
}

void workManager::Add()
{
    int pCount;
    cout<<"How much person you want to add: "<<endl;
    cin>>pCount;
    int newSize = pCount+workerNum;
    if(pCount>0)
    {
        worker ** addPerson = new worker*[newSize];
//指针也是可以创建数组的,用二级指针指向指针数组
        if (this->workerArray != nullptr)
        {
            for (int i = 0;i<this->workerNum;i++)
            {
                addPerson[i] = this->workerArray[i];
//指针数组本质也是数组,可以直接按数组形式来使用
            }
        }
        for (int j = this->workerNum; j<newSize;j++)
        {
            int id;
            string name;
            int partid;
            cout<<"Please input the id of the new one: "<<endl;
            cin>>id;
            cout<<"Please input the name of the new one: "<<endl;
            cin>>name;
            LABLE:
            cout<<"Please input the partid of the new one:1.ordinarystaff  2.managerstaff  3.boss "<<endl;
            cin>>partid;
            switch (partid)
//该switch就是这个案例要说的事情:通过多态形式的父类指针数组可以指向不同的子类对象,进而实现一个数组中存储不同类型数据的要求
            {
            case 1:
                addPerson[j] = new ordinaryStaff(id,name,partid);
                break;
            case 2:
                addPerson[j] = new managerStaff(id,name,partid);
                break;
            case 3:
                addPerson[j] = new boss(id,name,partid);
                break;
            default:
                cout<<"Wrong input..."<<endl;
                goto LABLE;
//goto语句有时候还是可以用一下的
                break;
            }
        }
        delete[] this->workerArray;
//指针数组对象可以用这个形式清空旧堆区上的数据
        this->workerArray = addPerson;
//此时workerArray指针也指向了新的指针的地址,所以可以取到新数据咯,真不错,不是赋值的意思哦
        this->workerNum = newSize;
        cout<<"Add succeed!!"<<endl;
    }
    else{
        cout<<"Wrong input"<<endl;
        return;
    }  
}

知识点:

        都在代码注释里了,虽然长,但是很好理解。其实就是一个虚基类+三个子类用类的继承和多态特性实现一些功能,然后有个封装类用来专门实现一些特定函数而已。

5. 数组空间存储注意事项

先看代码:

for (int j = index; j < this->workerNum-1; j++)
            {
                this->workerArray[j] = this->workerArray[j+1];
            }
            this->workerNum--;

知识点:

        数组的存储是连续的空间,指针数组也是数组,所以不能直接用delete prtArray[index] 来删除某一个数据,会报错。

        如果确实需要删除数组中间的某个数据,需要将后续数据进行迁移,实现覆盖删除。

6.常见的.hpp文件是什么呢

先看代码:

template<class T1, class T2>
class person
{
public:
    person(T1 name, T2 age);
    void msgShow();
    T1 m_name;
    T2 m_age;
};

template<class T1, class T2>
person<T1, T2>::person(T1 name, T2 age)
{
    m_name = name;
    m_age = age;
}

template<class T1, class T2>
void person<T1, T2>::msgShow()
{
     cout<<m_name<<m_age<<endl;
}

知识点:

        由于类模板中的成员函数的创建是在调用阶段(因为你在没有传入参数类型的时候,系统无法确定需要分给多少内存来创建),因此当.h和.cpp文件分开写的时候,主函数实现那个文件无法链接到.h文件声明的成员函数的具体实现。

        因此常用的解决方法是把类模板的成员函数的声明和实现写到一起,并将后缀改为.hpp文件(约定俗成,因此看到这个后缀的文件一般都是使用了类模板),然后在主函数实现文件中包含这个.hpp文件就行。

7. 使用类模板中需要new一块堆区数组,而对象是Person类时的报错问题;

先看代码:

class Person {
public:
    Person(std::string name) { /* 带参数的构造函数 */ }
};

template<typename T>
class MyClass {
public:
    T* arrAddress;
    int mCapicity;
    
    MyClass(int capicity) {
        // 试图动态分配 T 类型的数组
        this->arrAddress = new T[this->mCapicity];  // 如果 T 是 Person,会报错
    }
};

MyClass<Person> obj(10);  // 错误:找不到 Person 的默认构造函数
//修改后
class Person {
public:
    Person() { /* 默认构造函数 */ }
    Person(std::string name) { /* 带参数的构造函数 */ }
};
MyClass<Person> obj(10);//不报错

知识点:

        new在堆区开辟数组的时候,就会调用Person类中的默认构造函数,因为Person写了有参构造,覆盖了默认构造函数,所以要特意写默认构造函数。

8.STL的基本概念

知识点:

       STL全称是“标准模板库”,其核心是提供已经写好的标准数据结构和算法,其中容器是用来存放数据的,算法是用来解决问题的,迭代器是算法在解决问题的过程中访问容器中的数据时需要借助的桥梁。

        怪不得说C++是面向对象的编程,原来string的本质也是个类,内部封装这char *这个c语言样式的字符串实现,还包含着各种方法(查找、拷贝、删除等),也有多个重载构造函数,包括默认构造string()和各种传参构造string(const char *),string(const string & str),string(n, char)这些,就连赋值操作,都是别人已经写好了的成员函数/运算符重载(operator=)实现的。

9.STL各容器的特点性质

知识点:

       不同的容器类型有不同的应用场景,按需取用。可参考如下总结:stl容器使用场景汇总

        有个“pair对组”的小概念,适用于成对出现的数据,简易模式如下:

pair<string, int> p = make_pair("Tom", 20)
cout<<p.first<<p.second<<endl;

 10.函数对象

先看代码:

class myAdd
{
public:
    myAdd()
    {
        this->count = 0;
    }
    int operator()(int a, int b)
    {
       return a+b;
        count++
    }
    int count;    //可以拥有自己的状态
};

int main()
{
    myAdd ma;
    int result = ma(1,2);
    return 0;
}

知识点:

        函数对象又称仿函数,本质是类,即在类中对()进行重载,使得该类可以像函数一样调用;

        “谓词”概念:返回bool类型的仿函数称为谓词。

        使用:在内置函数中标为"Pred"的一般就是需要填一个谓词

 11.STL算法和内建函数对象

  知识点:

        STL还提供了很多标准且有用的算法以及内建函数对象,分别在头文件<algorithm>和<funtional>里,包括for_each算法、find_it算法和plus()加法内建函数对象、negate()取反内建函数对象等简单但常用的仿函数。比如在使用<algorithm>下的sort()算法时,有时想要从大到小排序,需要添加一个谓词,也就是对比实现,就可以用<funtional>下的greater<int>()内建函数对象。

#include<iostream>
#include<vector>
#include<algorithm>
#include<funtional>

int main()
{
    vector<int> v;
    v.push_back(10);
    v.push_back(30);
    v.push_back(20);
    sort(v.begin(),v.end(),greater<int>());
    //sort(v.begin(),v.end()) 
    //内部实现是sort(v.begin(),v.end(),less<int>());默认为less<int>()
    return 0;
}

12.容器遍历的简易方式

        除了常规的使用迭代器便利之外,还可以直接使用C++11引入的“冒号”遍历语法。

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> words = {"hello", "world", "example"};

    for (auto& c : words) {
        // 在这里可以对每个字符串进行操作
        std::cout << c << std::endl;
    }

    return 0;
}

   13. 枚举类型创建

枚举类型的关键字: enum;

使用场景:如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。所谓"枚举"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内。

示例代码:

enum _color
{
    blue,    //0
    red,    //1
    black    //2
} color;

//枚举的创建其本质是一种数据类型,与class或struct类型类似,enum _color color1 = blue;
//其中enum是创建枚举类型的关键字
//_color是枚举的类型,就是给这个枚举取个名字,这里表示颜色枚举
//color是_color类型的变量

还有一种常用的定义,引入typedef, 给类型定义别名

typedef enum _color
{
    blue,
    red,
    black
} color;

color color1 = blue;

14.数组和引用的关系

        先看报错:"cannot convert ‘double [PointsNum]’ to ‘double&’";大概意思是这个错误意味着在代码中试图将一个 数组 类型(如 double[PointsNum])传递给一个需要 引用(reference) 类型(如 double&)的函数参数。这是由于数组和引用类型在 C++ 中的性质不同引起的。

原因:

在 C++ 中:

  1. 数组类型 (double[PointsNum]) 是一个连续的内存块,表示多个 double 类型的值。
  2. 引用类型 (double&) 是对单个 double 值的引用,而不是一整个数组。

当函数参数声明为 double& 时,表示它希望接收的是一个 double 类型的变量,而不是数组。

15.成员初始化列表

        一种神奇的构造函数初始化成员参数的方式,语法如下:

ClassName() : member1(value1), member2(value2) {}

        完整举例如下:

ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}

为什么使用成员初始化列表?

  • 提高效率:成员初始化列表直接初始化成员变量,而不是先调用默认构造函数再赋值,避免了重复初始化。
  • 常量和引用类型:对于 const 类型或引用类型成员变量,只能通过初始化列表进行初始化,不能在构造函数体内赋值。
  • 更好的习惯:初始化列表可以明确表达初始化意图,特别是对指针或自定义对象类型。

---------------------------------------------------------------------------------------------------------------------

涉及一个概念:初始化和赋值

 

 

16.size_t数据类型

        在C++中,size_t是一个用于表示对象大小或数组索引的无符号整数类型,它是由标准库定义的类型。

typedef unsigned long size_t; // 这是常见的定义方式,但具体实现依赖于系统

为什么使用 size_t

  • 避免负值:由于 size_t 是无符号的,它不会包含负值,适合用于表示大小和长度等不能为负数的值。
  • 跨平台兼容性:使用 size_t 可以确保在不同的系统架构(如32位或64位)上有合适的大小表示内存和对象的长度。
  • 性能优化:由于 size_t 是无符号类型,某些编译器会对其进行优化,减少不必要的类型检查和转换。

常见问题类型转换警告

        当使用 size_t 与其他整数类型(如 int)进行比较时,可能会出现编译器警告,因为 size_t 是无符号类型,而 int 是有符号类型。

17.const---指针---容器迭代器

const-迭代器

const-指针

 18.const成员函数与mutable成员变量

 const成员函数

 mutable成员变量

 19.善用delete和delete[],预防内存泄漏

        使用指针指向某一个数据或新开辟的内存的时候,当其生命周期结束记得调用delete(删除对象数据),delete[](删除数组数据);

        还有一个解决办法是使用auto_ptr智能指针和tr1::shared_ptr智能指针,后者属于计数行智能指针,两者的共同点和特性是当其指向的对象在结束生命周期后可以在析构函数里面自动调用delete而无须手写,但是不会调用delete[]哦,需要注意;不同在于,前者的被复制对象指针指向nullptr,后者则不会出现该情况。

20.二分查找中一个关于➗2的小注意点

//传统找min
//会存在一个超出的问题,比如int类型的right已经是int里面的最大值了,再与left相加之后再除2就会出现错误
int min = (left+right)/2;


//新求法
//利用位运算,效果与传统方法一样,但是减法可以避免上面问题
int min = ((right-left)>>1)+left

21. 命名空间和显示声明构造

        把类放入namespace命名空间下实现,可以避免同名类的命名冲突,或增加可读性;

        显示(explicit)声明类的构造函数可以避免函数在接收参数传递时类型的隐形转换,显式声明构造函数能让代码的行为更加明确。隐式构造函数有时会导致代码理解上的困难,因为开发者可能不清楚某个值是如何转换为类对象的,尤其是对于复杂类型的转换,显式声明构造函数还能传达开发者的意图。如果构造函数是显式的,就能更好地表示“这个类只能通过明确的方式来初始化”,这有助于让后续的开发人员明确类的设计意图,减少滥用构造函数的风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值