Re:从零开始的 C++ 进阶篇(二)C++继承到底做了什么?从对象模型到底层内存布局彻底讲透

Re:从零开始的 C++ 进阶篇(二)C++继承到底做了什么?从对象模型到底层内存布局彻底讲透

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰

0.1概要&序論

这里是此方,好久不见。 继承是 C++ 中最核心却最易被误解的机制之一。它不仅关乎语法层面的扩展,更涉及对象模型、内存布局与多态实现。本文将从底层原理出发,系统解析继承的真实运作机制。这里是「此方」。让我们现在开始吧!

一,初识继承

1.1 继承的概念与使用方法导入

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在 保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为 派生类

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

下面我们看到两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数, 比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。

classStudent:publicPerson{public:voididentity(){//...;}voidstudy(){//...;}protected:int _stuid;};classTeacher:publicPerson{public:voididentity(){//...;}voidteaching(){//...;}protected: string title;};

下面是我们公共的成员都放到Person类中, 通过继承就可以复用这些成员,不需要重复定义了,省去了很多麻烦。

classPerson{public:voididentity(){ cout <<"void identity()"<< _name << endl; cout << _age << endl;}protected: string _name ="张三";// 姓名 string _address;// 地址 string _tel;// 电话private:int _age =18;// 年龄};

—— 如何实现继承呢? ——

1.2继承的定义格式

下面我们看到 Person 是基类,也称作父类;Student 是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类。)

继承的格式如下:通过某种继承方式将基类(右边)的成员继承给派生类(左边),这样派生类就拥有了基类的成员。中间用冒号隔开。

1.3继承的方式

访问限定符和继承方式采用的是同一套关键字。

先说结论,如下表,我们一部分一部分的拆开讲:

1.3.1基类private成员继承

基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。(确实继承下来了,但是再这个派生类中存在这个父类的private成员,但是不可访问。)

父类私有不能直接访问,但是可以间接访问我们可以用父类成员函数访问父类private成员(间接访问)。

1.3.2基类成员的protected继承

基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

1.3.3其他继承方式总结

实际上面的表格我们进行一下总结会发现。基类的私有成员在派生类都是不可见除此以外的继承我们有以下公式:( 其中:public >protected> private。


派生类使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

在实际运用中一般使用都是public继承,很少用protetected/private继承,也不提倡使用protetected/private继承,因为protetected/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

二,模板的继承

2.1适配器的另外一种实现方式

回想我们在学习queue和stack的时候提到过容器适配器。哪个时候我们是把被适配容器的对象在适配器里面留了一份,然后再基于这个对象的基础上封装各种接口:


但是也可以使用继承的方式来实现stack,如下我们将模板vector继承给stack。

template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){}voidpop(){vector<T>::pop_back();}const T&top(){}}

2.2模板继承和按需实例化引发的问题

2.2.1调用继承基类的函数时要显示指定依赖成员

首先,当我们在main中创建stack对象的时候,我们触发了stack的构造函数,然后我们也同时触发了vector的构造函数。但是这个时候不论是stack还是vector都没有实例化其他接口。

然后当我们调用push的时候我们根据按需实例化实例化了stack的push接口,但是我们没有实例化vector的push_back接口。此时是因为编译器不知道要实例化:模板成员函数在调用前不会实例化基类模板,所以你必须明确告诉编译器去依赖基类成员,否则编译器会报错。

stack<int> s; s.push(5);// 只实例化 stack<int>::push// 如果 push 内部调用 vector<T>::push_back(),此时才实例化 vector<int>::push_back
模板类继承模板类时,基类成员是 依赖名,编译器不会自动查找。调用依赖名成员有两种方法:① this->成员()。② 基类名<模板参数>::成员()。这样做是为了 按需实例化,避免不必要的模板展开,同时确保编译器知道成员存在。

2.2.2按需实例化导致的隐形错误

template<classT>classA{public:A(){}voidpush(const T& x){x.func();}};

去构造一个A对象,还是按需实例化,只构造这样一个类的对象,不会实例化 push 函数。 也就不会查出这个 push 里面有一个根本不可能被找到的函数 func()。 直到我们去调用它。

但是编译器又做了优化:

2013 版本的 VS 执行上述逻辑,不去检查没有被使用的接口。但是 2019 版本的 VS 执行的时候会要求:不依赖模板的会检查,依赖模板的不会去检查,如这个 x.func() 就不会被检查。

三,父子类对象的赋值兼容转换

3.1赋值兼容转换的定义

public继承的 **子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。**这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去。父类对象不能赋值给子类对象。

在这里插入图片描述


误区:这种赋值兼容转换没有发生类型转换。

在这里插入图片描述

赋值兼容转换的使用

赋值兼容转换代码案例如下,我们可以用一张图来说明这个问题:

在这里插入图片描述
intmain(){ Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用,但是父类对象不能给子类,会发生编译报错。 Person pobj = sobj; Person* pp =&sobj; Person& rp = sobj;return0;}

如图,我们引用的是子类当中父类的那一部分

在这里插入图片描述


父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用Run-Time Type Information的dynamic_cast来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下

四,继承中的作用域

4.1隐藏规则与实例解析

  • 在继承体系中基类和派生类都有独立的作用域。
  • 派生类和基类中有同名成员,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类::基类成员显示访问
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  • 注意在实际中在继承体系里面最好不要定义同名的成员。

我们拿实际案例来解释一下:

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆classPerson{protected:int _num =111; string _name ="小李子";// 姓名// 身份证号};classStudent:publicPerson{public:voidPrint(){ cout<<" 姓名:"<<_name<< endl; cout<<" 身份证号:"<<Person::_num<< endl; cout<<" 学号:"<<_num<<endl;}protected:int _num =999;// 学号};intmain(){ Student s1; s1.Print();return0;};

_num打印出来是 999 。因为在 Student 中:int _num = 999; 学号隐藏了 Person::_num

误区:隐藏绝非代表消失,而是影响了编译器的查找规则

4.2看两个选则题

1,A和B类中的两个func构成什么关系()
A. 重载 B.隐藏 C.没关系
2,下面程序的编译运行结果是什么()
A. 编译报错 B.运行报错 C.正常运行
classA{public:voidfun(){cout <<"func()"<< endl;}};classB:publicA{public:voidfun(int i){cout <<"func(int i)"<< i << endl;}};intmain(){ B b; b.fun(10); b.fun();return0;};

来,我们来分析一下,第一题,根据上面的隐藏规则,很显然函数名称一样,是构成隐藏。可能会有小伙伴选重载,这里要强调一下:重载必须是在同一个作用域。

第二题,还是根据隐藏,当调用fun(10)的时候会正常调用,但是在调用fun()的时候,编译器是找不到的,因为fun(int i)和父类的fun()名称相同构成隐藏。(也就是说,这儿你得指定一下作用域访问。)

——你,都做对了吗?——

再次忠告:不到万不得已不要在继承体系定义同名成员。

五,派生类的默认成员函数

5.1个常见默认成员函数

在类和对象章节中我们学习了6个默认成员函数,编译器会帮我们自动生成,那么在派生类中,这几个成员函数是如何生成的呢

在这里插入图片描述

5.1.1构造函数

派生类的构造函数 必须调用基类的构造函数初始化基类的那一部分成员。 如果基类没有默认的构造函数,则 必须在派生类构造函数的初始化列表阶段显示调用

解释一下,我们可以分块理解,子类继承了父类的成员,去构造子类的时候 子类的那一块儿要归子类构造,父类的那一块儿要归父类构造。
那么构造父类要调用父类的默认构造函数, 如果父类没有默认构造函数,就要像下面一样显示调用:

public:Student(constchar* name,int num,constchar* addrss):Person(name)// 显式调用父类构造函数,_num(num),_addrss(addrss){}protected:int _num =1;// 学号 string _addrss ="湖州市吴兴区";intmain(){ Student s("张三",1,"湖州市");return0;}

5.1.2拷贝构造

派生类的拷贝构造函数 必须调用基类的拷贝构造完成基类的拷贝初始化。

不用解释,我们直接看代码

Student(const Student& s):_num(s._num),_addrss(s._addrss),Person(s)// 拷贝父类部分{}protected:int _num =1; string _addrss ="湖州市吴兴区";};

如上,也同样在基类没有默认拷贝构造函数的时候 需要显示调用基类的拷贝构造函数。 而且这里还有区别,我调用基类的拷贝构造传递的只能是子类对象,所以 这里发生了赋值兼容转换。

细节:这里在调试的时候会发现,先执行 person(),原因是父类对象在被继承后的默认声明优先级最高。 先声明的先在初始化列表初始化。(为什么按照声明顺序?因为声明顺序就是内存当中的存储顺序,内存中父类在上子类在下。)

父类有默认的拷贝构造的时候不需要显示调用父类的拷贝构造。

5.1.3赋值重载

派生类的 operator=必须要调用基类的operator=完成基类的复制。 需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。

看代码:

Student&operator=(const Student& s){if(this!=&s){operator=(s); _num = s._num; _addrss = s._addrss;}return*t;}

这对吗?哈哈我就知道会有人这样写,我们运行一下看看:

在这里插入图片描述


出错了,我们调用堆栈看一下:函数发生了无限递归,为什么,我们来解释一下: 子类的operator=隐藏了父类的operator=, 导致的在子类的operator=中调用operator=调用的实际上还是子类的而不是父类的。
解决办法:加一个显示指定:

Person::operator=(s);

5.1.4析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。(看不懂没关系,后面多态章节会详细讲,现在只需要知道我们的析构函数是会在编译的时候统一成destructor()的)

直接看代码:

// destructor()~Student(){Person::~Person();}

肯定有人会想说:既然析构函数会在编译的时候统一成destructor(),那么基类析构函数就会被子类析构函数隐藏,所以必须显示调用基类析构函数
这样说没有错,但是我们运行一下就会出现问题:

在这里插入图片描述


为什么只有三个子类对象却调用了6次父类的析构函数?因为子类析构的时候会自动调用父类的析构函数,这里和构造函数尤其不同。,而我们又显示调用了一次父类的析构函数,于是就出现了重复调用。

5.1.5总结

派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。如下图:

按照这个我们来解释一下,为什么祖师爷会把基类的析构函数设计成自动调用,而构造函数不会:构造函数在初始化列表显示调用基类的析构函数,这个时候编译器会自动根据声明顺序调用基类还是子类的构造。但是析构函数就不一样了,如果认为操作析构函数,可能会先让基类先析构,子类后析构。

打开反汇编我们也会发现,事实确实如此:

在这里插入图片描述

5.2实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。报错类型如下:

在这里插入图片描述

方法2:C++11新增了一个final关键字表示最终类,final修饰基类,派生类就不能继承了。

classBasefinal{public:voidfunc5(){ cout <<"Base::func5"<< endl;}protected:int a =1;private:// C++98的方法Base(){}};classDerive:publicBase{public:voidfunc4(){ cout <<"Derive::func4"<< endl;}}

六,继承与友元&继承与静态

6.1继承与友元

友元关系不能继承,函数是基类的友元但不是子类的友元,也就是说基类友元不能访问派生类私有和保护成员 。
错误示范:

classStudent;classPerson{public:friendvoidDisplay(const Person& p,const Student& s);protected: string _name;// };classStudent:publicPerson{protected:int _stuNum;};voidDisplay(const Person& p,const Student& s){ cout << p._name << endl; cout << s._stuNum << endl;}

如上代码,加个前置声明是为了 Display函数的形式参数能够找到。 这里的Display函数访问了子类和父类的受保护成员,这种情况下,Display是基类的友元,但是不是子类的友元导致了访问必然发生错误。
解决办法是在子类里面也加一个友元声明。

6.2继承与静态

父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个static成员实例。

  • 普通成员被继承下来后子类里面有一个父类继承过来的,父类里面有一个原生的,这是两个成员。
  • 静态成员被继承下来后 子类里面的和父类里面的是同一个。
classPerson{public: string _name;staticint _count;};int Person::_count =0;classStudent:publicPerson{protected:int _stuNum;};intmain(){ Person p; Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份 cout <<&p._name << endl; cout <<&s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员 cout <<&p._count << endl; cout <<&s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员 cout << Person::_count << endl; cout << Student::_count << endl;return0;}

上面的测试代码运行结果

在这里插入图片描述


public下的static成员可以被其他类访问。换一种理解方式,父类中的静态变量只是受到父类类域限制的全局变量。

七,多继承原理与菱形继承困境

7.1 继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承.多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。菱形继承:菱形继承是多继承的一种特殊情况。 菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

概念看不明白?没事儿我们逐条结合图片解释一下你就懂了:

7.1.1单继承和多继承模型图解

在这里插入图片描述

7.2菱形继承模型详解

这个东西有说法,得单独拿出来讲讲。如下,这个assistat继承了两份Person类,一个来自Student,一个来自Teacher。于是引发了数据冗余和二义性。

在这里插入图片描述


于是肯定有人要问了:什么是数据冗余和二义性?

7.2.1数据冗余与对象空间膨胀

这个assistat继承了两份Person类,一个来自Student,一个来自Teacher。也就是说,在assistant类的内存中,存放了两份Person的成员。然而这不是必要的。

7.2.2二义性与逻辑语义崩溃

这个说实话不太好理解。我举个例子吧:

人 Person
/ \

男性 Male 女性 Female\ /某个人 Child

  • 假设 Person 类中存在一个成员变量:sex
  • 那么 Male 继承 Person 后,就会拥有 sex 成员,并且它的含义可以理解为:sex = man
  • 同理,Female 继承 Person 后也会拥有同一个成员:sex = woman

那么问题来了,这个孩子继承了这两个类,他同时具备sex=man和sex=woman,那么他到底是男孩还是女孩?

程序已经无法给出一个唯一答案,这就产生了 二义性.

更重要的是,这不仅仅是编译层面的歧义,==还会导致逻辑语义上的崩溃:==一个具体的人,在现实语义中只应该拥有 一个性别属性,而由于菱形继承的结构,却在对象内部产生了 两份 Person 对象,从而导致出现两个 sex 成员,这显然违背了对象模型本身的语义设计。
在这里插入图片描述
// 编译报错:error C2385:对"_name"的访问不明确 Assistant a; a._name ="peter";

需要 显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。

7.2.3库里面的菱形继承

菱形继承虽然不建议使用,但是库里面有一个经典的菱形继承,这个问题我们后面会讲。

在这里插入图片描述

7.3虚继承原理

7.3.1菱形虚拟继承

菱形继承 的出现引发了数据冗余和二义性的问题,为了解决这个问题C++引入了虚继承的概念。

classPerson{public: string _name;// 姓名};// 使⽤虚继承Person类classStudent:virtualpublicPerson{protected:int _num;//学号};// 使⽤虚继承Person类classTeacher:virtualpublicPerson{protected:int _id;// 职⼯编号};classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程intmain(){// 使⽤虚继承,可以解决数据冗余和⼆义性 Assistant a; a._name ="peter";return0;}

如上代码,使用虚继承解决数据冗余和二义性问题的本质是在于:虚继承后,Assistant对象继承的两个类公用一个Person类。

虽然菱形虚拟继承正确,但是非必要不要去设计菱形虚拟继承。 在代码理解和时间复杂度上都不友好。(你会被同事骂死)

7.3.2虚拟继承误区介绍

误区一:虚继承对student和teacher在使用方面没有影响。只对最下面这个类有影响。
误区二:只加上一个virtual不能解决问题。两个都得加。
误区三:
如下看图,这也算菱形继承,那么问题来了,这种菱形继承我们的virtual应该加在哪里才能防止E继承后出现数据冗余和二义性?

在这里插入图片描述


答案是加在B和C的位置。而不是D的位置,原因是引发数据冗余和二义性的数据是源自A类的,在这种情况下,只有在继承自A类的时侯加上virtual才能解决问题。

数据冗余的数据源自哪里就在继承的时候用虚继承

7.3.3总结虚拟继承

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。

上面讲的IO库中的经典菱形继承就是采用了虚拟继承。 源码如下:

template<classCharT,classTraits= std::char_traits<CharT>>classbasic_ostream:virtualpublic std::basic_ios<CharT,Traits>{};template<classCharT,classTraits= std::char_traits<CharT>>classbasic_istream:virtualpublic std::basic_ios<CharT,Traits>{};
在这里插入图片描述

7.4多继承中的指针偏移问题

讲这个之前我们来做一个小游戏: 在下面的选项种挑一个对的:

在这里插入图片描述
classBase1{public:int _b1;};classBase2{public:int _b2;};classDerive:publicBase1,publicBase2{public:int _d;};intmain(){ Derive d; Base1* p1 =&d; Base2* p2 =&d; Derive* p3 =&d;}return0;

答案是C,你作对了吗?我们来讲解一下:

在这里插入图片描述

如上图,我们解释一下:首先先继承的在前面,后继承的在后面。class Derive : public Base1, public Base2 所以Base1是先继承的,Base2是后继承的。

在子类的内存布局种,先继承的在最上面,后继承的其次,子类原有成员最后。 于是就出现了如上的内存情况,由此就可以得出结论:Base1和子类的开始指针一致。Base2与其他两者不一致。

*如下,我们打开内存布局和监视来印证一下结果:

在这里插入图片描述

八,继承与组合

8.1什么是组合&&继承和组合的区别

先看一段代码:

组合: classstack{private: list _lt;} 继承: classstack:publiclist{}
  • 第一个代码,我们将一个基类的对象直接声明在子类里面。这就是组合。
  • 第二个代码,我们将基类直接继承给子类,这就是继承。

专业点说,有以下两条说明:

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

8.1.1什么是is-a什么是has-a关系?

两句话你就明白了:
is-a:我是一名大学生。has-a:我有一颗心脏。

8.1.2黑箱与白箱——继承与组合的本质区别

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)

" 白箱"

术语“白箱”是相对可视性而言
:在继承方式中,基类的 内部细节对派生类可见。 继承一定程度 破坏了基类的封装 ,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合 要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse)。

" 黑箱"

因为对象的 内部细节是不可见的。 对象只以“黑箱”的形式出现。组合类之间 没有很强的依赖关系 ,耦合度低。优先使用对象组合有助于你保持每个类被封装。

总结: 黑盒测试:不了解底层实现,从功能角度测试白盒测试 (难):了解底层实现,从代码运行逻辑角度测试

我还想要补充一点:
继承的接口不论是保护还是开放都被用。组合不一样,组合接口只有开放的能被使用,开放的越少耦合度越低。

8.1.3 总结观点

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!

Read more

【C++详解】C++ 智能指针:使用场景、实现原理与内存泄漏防治

【C++详解】C++ 智能指针:使用场景、实现原理与内存泄漏防治

文章目录 * 一、智能指针的使⽤场景分析 * 二、RAII和智能指针的设计思路 * 三、C++标准库智能指针的使⽤ * 四、智能指针的原理 * shared_ptr源码 * 五、shared_ptr和weak_ptr * shared_ptr循环引⽤问题 * weak_ptr * 六、内存泄漏 * 什么是内存泄漏,内存泄漏的危害 * 如何避免内存泄漏 一、智能指针的使⽤场景分析 我们知道C++是是公认的高效编程语言,其中一点原因就是C++手动内存管理(new/delete),避免了很多高级语言(如 Java、Python)的自动内存管理(垃圾回收)带来的额外开销,这也是一把双刃剑,这对C++程序员的要求就会更高,因为手动内存管理很容易出现内存泄漏的问题,我们之前的说法是尽可能小心,但是有些场景无法避免会出现内存泄漏(或者处理起来很麻烦)

By Ne0inhk
【C++】 —— 笔试刷题day_18

【C++】 —— 笔试刷题day_18

一、压缩字符串(一) 题目解析 题目给定一个字符str,让我们将这个字符串进行压缩; **压缩规则:**出现多次的字符压缩成字符+数字;例如aaa压缩成a3。如果字符值出现一次,1不用写。 算法思路 这道题总的来说就非常简单了,我们直接模拟整个过程即可。 思路: 示例双指针遍历,统计字符和字符出现的次数; i固定一个字符,j向后遍历找与i位置相同的字符,如果相同就继续向后遍历,直到j位置与i位置的字符不相同; j向后遍历结束,i位置字符出现的字符次数为j-i;如果j-1大于1就在结果字符串中加入出现的次数;等于1则不用加次数。 代码实现 classSolution{public: string compressString(string param){ string ret;for(int i =0;i<param.size();){int j = i+1;while(j<

By Ne0inhk

C++:实现字符串分割split函数(附带源码)

项目背景详细介绍 在实际的软件开发过程中,字符串处理是最基础、也是最常见的需求之一。无论是系统底层开发、网络通信、日志分析,还是 Web 后端、工具类程序,字符串的解析与拆分都无处不在。 在很多高级语言中(如 Python、JavaScript、Java),字符串分割函数是语言内建能力: * Python:str.split() * Java:String.split() * JavaScript:String.split() 然而在 C++ 标准库中,并没有一个直接、统一、易用的 split 函数。这就导致: * 初学者不知道如何优雅地拆分字符串 * 面试和笔试中 split 函数几乎是“必写题” * 工程中经常需要重复实现自己的 split 工具函数 因此,实现一个通用、健壮、可扩展的 split 函数,

By Ne0inhk
C++进阶:(十六)从裸指针到智能指针,C++ 内存管理的 “自动驾驶” 进化之路

C++进阶:(十六)从裸指针到智能指针,C++ 内存管理的 “自动驾驶” 进化之路

目录 前言 一、裸指针的 “血泪史”:为什么我们需要智能指针? 1.1 内存泄漏:最常见的 “噩梦” 1.2 二次释放:致命的 “双重打击” 1.3 野指针:潜伏的 “幽灵” 1.4 异常安全:被忽略的 “隐形杀手” 1.5 智能指针的核心使命 二、智能指针的 “三驾马车”:unique_ptr、shared_ptr、weak_ptr 2.1 unique_ptr:独占所有权的 “独行侠” 2.1.1 unique_ptr 的核心原理

By Ne0inhk