C++:类继承知识回顾
概述
在实际代码开发中,我们通常不会去开发最底层,而是成为“调库侠”。面对众多类库,我们需要掌握基本库的用法,比如string
、valarray
、iostream
、any
等,本白在开发capl测试工程框架时,也是经常去调用vector自定开发的一些cin文件,比如scope,capldll,TDK等,因此掌握类的基本开发、使用、注意事项是十分必要的。《C++ plus》的第十三章正好可以为自己解答一部分含糊不清的概念,从如何用深入到为什莫这莫用。除此之外,也为后续以太网的学习和服务器开发练习打基础。下面就从《C++ plus》中提供的部分练习去了解相关概念,顺便对这两周的业余学习做个总结。
类继承代码
baseDMA.h
#pragma once #include <iostream> #include <cstring> using namespace std; class baseDMA { private: char* label; int rating; public: baseDMA(const char *pa = "null", int r = 0); /*动态内存分配必须定义一个复制构造函数用来new*/ baseDMA(const baseDMA &rs); virtual ~baseDMA();//虚析构函数,用来释放new的类对象空间,或者只能显示析构 baseDMA& operator =(const baseDMA &s);//赋值运算符重载 friend ostream& operator <<(ostream &os,const baseDMA &ra ); }; /*派生类*/ class plusDMA : public baseDMA { private: enum {colorlen=40}; char color[colorlen]; public: plusDMA(const baseDMA &ra,const char *co = "null"); //*8virtual ~plusDMA();/*没有new,因此析构可以不写,也不用定义复制构造韩式,也不需要重载赋值运算符*/ friend ostream& operator <<(ostream& os, const plusDMA& ra); }; class clckDMA : public baseDMA { private: char* style;//深拷贝 new public: clckDMA(const char* l = "null", const char* s = "none style"); clckDMA(const baseDMA& rs,const char *s="none style"); /*需要进行复制构造函数的显式定义,因为默认构造函数没有new的操作*/ clckDMA(const clckDMA& cs); clckDMA& operator = (const clckDMA & cs); friend ostream& operator <<(ostream& os, const clckDMA& cs); virtual ~clckDMA();//析构 虚函数 };
baseDMA.cpp
#include "类继承与动态内存.h" baseDMA::baseDMA(const char* pa, int r) { this->label= new char[strlen(pa)+1]; strcpy_s(this->label, strlen(pa) + 1,pa); this->rating = r; } /*动态内存分配必须定义一个复制构造函数用来new*/ baseDMA::baseDMA(const baseDMA& rs) { this->label = new char[strlen(rs.label) + 1]; strcpy_s(this->label, strlen(rs.label) + 1, rs.label); this->rating = rs.rating; } baseDMA:: ~baseDMA() { delete[] this->label; } baseDMA& baseDMA::operator=(const baseDMA& s)//赋值运算符重载 { /*自我检查*/ if (this == &s) return *this; /*释放s类之前的指针空间*/ delete[] this->label; this->label = new char[strlen(s.label) + 1]; strcpy_s(this->label, strlen(s.label) + 1, s.label); this->rating = s.rating; return *this; } ostream& operator <<(ostream& os, const baseDMA& ra) { os << "this label:" << ra.label << endl; os << "this rating:" << ra.rating << endl; return os; } ostream& operator <<(ostream& os, const plusDMA& ra) { os << (const baseDMA &)ra/*取出派生类的基类*/; os << "this color:" << ra.color << endl; return os; } //列表初始化 plusDMA::plusDMA(const baseDMA& ra, const char* co) : baseDMA(ra) { strcpy_s(this->color, 38, co); this->color[39] = '\n'; } //派生类clckdma的函数定义,使用列表初始化进行 clckDMA::clckDMA(const baseDMA& rs, const char* s) : baseDMA(rs) { cout << "---------clckDMA---------" << endl; this->style = new char[strlen(s)+1]; strcpy_s(this->style, strlen(s) + 1,s); } clckDMA& clckDMA::operator = (const clckDMA& cs) { cout << "赋值运算符重载!" << endl; if (this == &cs) return *this; /*释放赋值对象的style指针之前指向的内内容*/ delete[] this->style; this->style = new char[strlen(cs.style) + 1]; strcpy_s(this->style, strlen(cs.style) + 1, cs.style); /*基类的对象赋值:需要注意不能直接this=&cs,这样会无限递归,下面必须使用基类的赋值运算符重载*/ //*this = baseDMA::operator=(cs); 不能这样写,因为第一个=会调用自身 这样会无限递归 baseDMA::operator=(cs); return *this; } ostream& operator <<(ostream& os, const clckDMA& cs) { os << (const baseDMA&)cs/*取出派生类的基类*/; os << "this style:" << cs.style << endl; return os; } /*需要进行复制构造函数的显式定义,因为默认构造函数没有new的操作*/ clckDMA::clckDMA(const clckDMA& cs) : baseDMA(cs) { cout << "复制构造函数!" << endl; this->style = new char[strlen(cs.style) + 1]; strcpy_s(this->style, strlen(cs.style) + 1, cs.style); } clckDMA::~clckDMA() { delete[] this->style; } int main(void) { baseDMA SHIRT("4564865",8); cout << SHIRT; //派生类没有new plusDMA shift(SHIRT, "YEELOW"); cout << shift; //派生类动态new clckDMA cls(SHIRT,"hei"); cout << cls; clckDMA clss = cls;//复制构造函数(拷贝函数) cout << clss; clss = cls; cout << clss;//赋值运算符重载 return 0; }
代码解释
上述两个文件共同实现了基类baseDMA
与派生类plusDMA
的声明与定义,囊括的知识点包括类定义、类继承、抽象类、类的动态内存管理、虚函数、早期/晚期联编、赋值运算符、赋值构造函数、初始化参数列表、类的引用传递与按值传递区别、类的友元函数,暂时就想到这莫多。下面将逐一回顾标红的部分,当然一些简单的类定义与类继承就不说啦,书本里说的都很透彻了:
抽象类
含有虚函数的对象就叫做抽象类,其实你可以理解为包含虚函数的基类,因此抽象类也叫抽象基类,只是利用虚函数增加了类函数的动态性,使用上和你平时定义与使用基类没有什么区别。从定义看好像没有必要专门把抽象类拿出来单拉一章来讲,C++之所以这样做的目的是因为抽象类的使用场景和平时所说的一对一的基类-派生关系稍微有点不同。
举个例子:引自http://t.csdn.cn/GlMGs
可以看见,当多个类拥有共同的属性时,这个时候我们应该考虑定义一个抽象基类,抽象基类中包括信用卡类与储存卡类的相同基本信息属性,比如姓名、年龄等。并且当我们访问抽象基类的成员时需要通过public定义的类成员函数去virtual void show()
访问,而当在不同类去访问成员时,我们肯定也是在信用卡类和储存卡类中分别定义了virtual void show()
,并且在这个show
中显式访问基类的show
函数,这样就基本实现了所谓的抽象类啦~说到这里其实也明白抽象类其实只是一种编程的思想与方法,除virtual
外并没有什么特别的语法,因此回到开始,包含virtual
一定是抽象类!小白本人目前就这样理解的,不足之处大家可以提出。
类的动态内存管理
这是极其重要的一种思想!!参考上面的例子,如果我们定义一个成员姓名时,如果你是从C转过来的小伙伴,肯定就直接char[]
,这样其实不太好,因为成员的姓名长度不一,当成员数较多时内存消耗极其大,当然这种方法是可行的。可是你都上C++啦!!我们就要用更加高大上的方法,直接string
或者valarray
,或者char * name
;如果你很幸运的选择了第三者定义字符串指针的做法,那就必须要考虑内存管理了,嘿嘿。
其实内存管理很简单,就是有new
必有delete
,根据你new
的不同释放时注意delete
和delete[]
就好了。
我们来看文章最初的部分代码:
baseDMA::baseDMA(const char* pa, int r) { this->label= new char[strlen(pa)+1]; //new [] strcpy_s(this->label, strlen(pa) + 1,pa); this->rating = r; }
这里采用了new []
,因此我们释放时一定要delete[]
。
baseDMA:: ~baseDMA() { delete[] this->label; //delete[] }
完事了?不!我们必须还要注意赋值运算符重载和复制构造函数的情况!因为你要是考虑没这两种情况,一旦将类与类之间 =
或者 按值传递引起默认复制构造函数
时,是不是少了一个new
呀?这个时候C++编译器就会报null错误了!(对了还有static
的情况)所以我们要:
/*动态内存分配必须定义一个复制构造函数用来new*/ baseDMA::baseDMA(const baseDMA& rs) { this->label = new char[strlen(rs.label) + 1]; strcpy_s(this->label, strlen(rs.label) + 1, rs.label); this->rating = rs.rating; } baseDMA& baseDMA::operator=(const baseDMA& s)/*动态内存分配必须定义一个复制构造函数用来new*/ baseDMA::baseDMA(const baseDMA& rs) { this->label = new char[strlen(rs.label) + 1]; strcpy_s(this->label, strlen(rs.label) + 1, rs.label); this->rating = rs.rating; } baseDMA:: ~baseDMA() { delete[] this->label; } //赋值运算符重载 baseDMA& baseDMA::operator=(const baseDMA& s) { /*自我检查*/ if (this == &s) return *this; /*释放s类之前的指针空间*/ delete[] this->label; this->label = new char[strlen(s.label) + 1]; strcpy_s(this->label, strlen(s.label) + 1, s.label); this->rating = s.rating; return *this; } { /*自我检查*/ if (this == &s) return *this; /*释放s类之前的指针空间*/ delete[] this->label; this->label = new char[strlen(s.label) + 1]; strcpy_s(this->label, strlen(s.label) + 1, s.label); this->rating = s.rating; return *this; }
注意在重载赋值运算符时一定要自我检查,并且删除类本身指向的空间~以免造成内存泄漏,也可以说搞了一个野指针出来!
虚函数与早期/晚期联编
虚函数经过抽象类的介绍,应该有点概念,就是实现接口的动态调用。并且虚函数还有一个功能,就是调用的类成员函数不再取决于指针或引用本身的类型,而是取决指向的对象类型!虚函数也对应了晚期联编(动态联编),就是程序执行到对应虚函数时会再决定调用基类还是派生类的函数,因此非虚函数都是早期联编(静态联编)。可见晚期联编会消耗一定内存和性能,因此晚期联编还是早期联编各有优劣,大家知道就好啦。
当然,虚函数的重要也体现在析构函数中,虚函数应用于析构函数时,会规定好派生类和基类的析构顺序,这一点对于new
和delete
是极其重要的,为了良好的开发习惯,一定要将基类的析构函数定义为虚函数!!!
类的按值传递和引用传递的区别
前面也提到过,类当按值传递时会调用默认的默认复制构造函数,当你在构造函数使用了动态内存管理时,需要注意重新显式定义复制构造函数和赋值运算符。其实按值传递和引用传递的区别之一就是深拷贝和浅拷贝的区别,如果你按值传递,就不需要深拷贝,但是当你使用引用或者指针时,就需要考虑new
以此进行深拷贝。个人建议最好引用传递,因为引用传递占用的内存更小,更安全,因为内存管理是C++最重要的性能之一,C++的内存安全是我们开发时必须必须要注意的。按值传递也可以,如果你的需求不大并且你的电脑也很强劲!
深拷贝和浅拷贝
- 深拷贝:新建空间,拷贝一个完全体放到里面,注意两个指针各管各的,里面内容也完全一致
- 浅拷贝:不新建空间,只是新建一个变量,将源对象地址再复制到此变量里面,注意此时就是两个指针管理一个内存空间
总结
相信看到这里对类的一些基本用法和注意事项有了简单的了解,至于运算符重载、友元函数、多重类继承书本里同样很详细的介绍过了,就不再赘述,那本白的回顾到此结束!另外对于本文所引用图片,有一部分是采用之前的笔记,因此部分作者链接找不到了,很抱歉!
本文来自博客园,作者:{张一默},转载请注明原文链接:https://www.cnblogs.com/YiMo9929/p/16928586.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)