导图社区 1、侯捷笔记:Cpp面向对象开发
Cpp面向对象开发重点知识总结,包括面向对象的高级开发、程序设计兼谈对象模型两部分内容。
编辑于2022-05-19 11:35:47C++
面向对象的高级开发
目标
单一对象
头文件与类的声明
防卫式声明
头文件布局
class的声明
模板简介
内联函数
访问级别
public
protect
private
classes的两个经典分类
Complex(without pointer member(s))
构造函数 (Complex复数类(不带指针的类))
构造函数初值列效率高于赋值
C++重载overloading可以有多个,因为编译时编译为不同名称
注意:已有默认参数和没有参数的构造函数不能同时存在,因为创建对象时编译器无法判断调用哪个构造函数
构造函数放入private区,不允许外界创建,单例(singleton)
常量成员函数
不改变数据,函数能加const一定要加,不然下面的const对象调用函数会出错。 因为对象定义为常量是不能改变的,但函数认为对象的内容是可以改变的,矛盾。
参数传递
尽量传引用,引用底部相当于指针
传引用不希望对方改,加入const修饰
返回值
返回值尽量也传递引用
friend(友元)
友元可以直接取private数据
相同class的各个objects互为friends(友元) 所以可以打破封装
总结:写一个类应该注意什么
1、构造函数特殊语法initialization list(初值列)
complex(double r=0,double i=0):re(r),im(i){}
2、在类的函数里面应该加const的加const,不然使用者使用的时候,编译器可能会报错,(包含函数内的参数或对函数体加)(一般构造函数、拷贝构造、拷贝赋值等不加const,因为会改变类内数据)
3、参数尽可能以reference传,返回值也是尽可能reference
4、数据一定放在private里
什么时候不能用引用呢 一般情况下把临时对象当引用传递,如:a=b+c (b+c 返回一个临时对象) 如果不是在函数内创建临时对象,那么返回值一般可以带有引用
操作符重载与临时对象
操作符重载有两种写法: 1、成员函数 2、非成员函数 特殊的是:<<重载运算符只能是非成员函数
<<去除成员函数的情况
this指针
成员函数
+=运算符考虑重载运算符的时候返回值应该为complex&,而不能是void 不然当有多个连续的+=运算时会报错,因为返回值为void,不能再进行+=运算
非成员函数
为了对付client的三种可能用法,开发三个函数(非成员函数) 因为返回值为临时对象所以返回值不能是引用。
注意: << 操作符重载是标准库内特殊的操作符重载,只能识别当时<<使用时有的操作,不能识别新的操作,因为只能定义为非成员函数。
现考虑这个<<非成员函数怎么写 1、参数传递是不是引用 2、传递进来的参数需不需要改变(加不加const) 3、返回值是什么(void/非void), <<重载运算符返回值不能为void,防止后续还要追加数据到屏幕 cout<<c1<<conj(c1); 4、返回值能不能加引用(还可以考虑加不加个inline) 5、该函数能不能加const。
Complex类代码
#ifndef __Complex__ #define __Complex__ #include<iostream> using namespace std; //using std::cout; //using std::ostream; /*前向声明*/ //class ostream; class Complex; Complex& _doapl(Complex* ths, const Complex& r); /*类声明*/ class Complex { public: Complex(double a=0,double b=0):re(a),im(b){} double real() const { return re; } double imag() const { return im; } Complex& operator+=(const Complex& r); //Complex& _doapl(Complex* ths, const Complex& r); private: double re, im; friend Complex& _doapl(Complex* ths, const Complex& r); }; /*类定义*/ //这个应该就是class类内互为友元 //inline Complex& Complex::_doapl(Complex* ths, const Complex& r) { // ths->re += r.re; // ths->im += r.im; // return *ths; //} //此函数如果不是友元函数就不能访问复数类中的私有成员变量 inline Complex& _doapl(Complex* ths, const Complex& r) { ths->re += r.re; ths->im += r.im; return *ths; } //+=运算符考虑重载运算符的时候返回值应该为complex&,而不能是void //不然当有多个连续的 += 运算时会报错,因为返回值为void,不能再进行 += 运算 inline Complex& Complex::operator+=(const Complex& r) { return _doapl(this, r); } /*非成员函数的定义*/ inline double imag(const Complex& x) { return x.imag(); } inline double real(const Complex& x) { return x.real(); } //为了对付client的三种可能用法,开发三个函数(非成员函数) //因为返回值为临时对象所以返回值不能是引用 inline Complex operator+(const Complex& x, const Complex& y) { return Complex(real(x) + real(y), imag(x) + imag(y)); } inline Complex operator+(const Complex& x, double y) { return Complex(real(x) + y, imag(x)); } inline Complex operator+(double x, const Complex& y) { return Complex(x + real(y), imag(y)); } //反相取反 inline Complex operator+(const Complex& x) { return x; } //绝不可以return by reference,因为其返回的必定是个local object inline Complex operator-(const Complex& x) { return Complex(-real(x), -imag(x)); } inline bool operator==(const Complex &x, const Complex& y) { return real(x) == real(y) && imag(x) == imag(y); } inline bool operator==(const Complex &x, double y) { return real(x) == y && imag(x) == 0; } inline bool operator==(double x, const Complex& y) { return x == real(y) && 0 == imag(y); } inline bool operator!=(const Complex &x, const Complex& y) { return real(x) != real(y) || imag(x) != imag(y); } inline bool operator!=(const Complex &x, double y) { return real(x) != y || imag(x) != 0; } inline bool operator!=(double x, const Complex& y) { return x != real(y) || 0 != imag(y); } inline Complex conj(const Complex& x) { return Complex(real(x), -imag(x)); } ostream& operator<<(ostream& os, const Complex& c) { return os << '(' << real(c) << ',' << imag(c)<<')'; } int main() { Complex c1(2, 1); Complex c2(5); c2 += c1; c2 += c2 += c1;//+=重载返回值不能是void //client的三种可能用法 c2 = c1 + c2; c2 = c1 + 5; c2 = 7 + c1; // cout << c2 << endl; cout << -c1 << endl; cout << +c1 << endl; cout << (c1 == c2); cout << (c1 == 2); cout << (0 == c2); } #endif
string(with pointer member(s))
三大函数:拷贝函数、拷贝赋值、析构函数
注意:拷贝赋值函数的检测自我赋值不止为了效率,也为了正确
如果没有检测自我赋值 执行第一步删除自己的空间后,后面的几步都将出错
output函数
String类代码
#ifndef __MYSTRING__ #define __MYSTRING__ #pragma warning(disable:4996) #include<iostream> #include<cstring> using namespace std; class String { public: String(const char* cstr = 0);//构造函数 //带有指针的类需要考虑3个特殊函数 String(const String& str);//拷贝构造 String& operator=(const String& str);//拷贝赋值 ~String();//析构函数 char* get_c_str() const { return m_data; };// private: char* m_data; }; inline String::String(const char* cstr) {//构造函数,声明时有默认,定义时不用写默认 if (cstr) { m_data = new char[strlen(cstr) + 1]; strcpy(m_data, cstr); } else {//未指定初值 m_data = new char[1]; *m_data = '\0'; } } //析构函数 inline String::~String() { delete[]m_data; } //拷贝构造函数,class兄弟之间互为友元 inline String::String(const String& str) { m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); } //拷贝赋值函数 //delete[] 首先是调用析构函数释放所有的堆空间,其次是释放指针空间 //new 首先是创建对应的空间,这里的strlen(str.m_data)应该是在堆中检索字符指针所指对象的空间,+1为加一个'\0'结束符 //然后强制转换指针再使用构造函数,是数组的话就调用几次构造函数 inline String& String::operator=(const String& str) {//&引用 if (this == &str)//&取地址 return *this;//检测自我赋值 delete[] m_data; m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); return *this; } ostream& operator<<(ostream& os, const String& str) { os << str.get_c_str(); return os; } #endif int main() { String s1;//不能为String s1(); String s2("hello"); String s3(s1); cout << s3 << endl; s3 = s2; cout << s3 << endl; }
堆、栈与内存管理
new:先分配memory,再调用ctor
3动作
delete:先调用dtor,再释放memory
2动作
在VC的调试模式下,分配给类的大小为变量大小+灰色部分(36B:32+4)+上下面的cookie(8B)+(填充的字节) (最后能够被16整除) 在非调试模式下,release模式下,没有灰色部分 上下面的cookie用来记录分配的大小, 上面cookie的后4位用于表示是否分配,1表示分配,0表示没分配,如:上面的红色cookie 00000041的4表示分配了64字节,1表示已分配
array new 一定要搭配 array delete(动态分配内存)
如下图,如果new一个数组后不delete数组([]p)会造成内存泄漏,原因如下:
1、首先泄漏的不是指针所占的空间(下图灰色区),因为当析构函数结束后,整个类空间都会给删除,包括指针所在的空间(下图灰色区),
2、因为delete时分为两步, 第一步是析构函数,释放指针所指的堆地址空间。如果写成delete p而不是delete[] p,就只进行了一个对象的释放,而其余的堆空间就不知道释放了。 第二步是释放空间,因为栈知道分配空间的大小,所以栈内的空间正常释放。
static补充
静态函数没有this指针,只能处理静态数据,静态数据
调用static函数的方式有二:
通过object调用
通过class name调用
饿汉模式的单例(清晰简单)
直接创建个静态对象,可以改进
把静态对象的创建放在静态函数中
cout补充
class template,类模板
function template,函数模板
(模板关键字相通:typename和class)
函数模板会对参数进行推导,如图所示,不需要告知T
进一步补充:namespace
更多细节
多个对象之间的关系
继承(Inheritance)、复合(Composition)、委托(Delegation)
复合我有一个东西(has-a),有东西对象
Adapter(适配器) queue由deque改造,可以说queue是一个适配器
复合关系下的构造和析构,构造由内而外的执行,析构是由外而内执行
委托(Delegation),UML中菱形标志,有指向对象的指针
Handle/Body(pIml)
继承(Inheritance),表示is-a
从内存的角度分析
子类的内存中包含父类中的一部分
继承之虚函数
非虚函数、虚函数、纯虚函数
虚函数的调用
创建子类,子类调用父类方法,父类方法中含有子类重写的虚函数,观察下图的顺序(Template Method)
继承+复合下的构造和析构
1、创建子类对象时,如果子类包含父类和组合对象,先调用父类的构造函数在调用组合对象的构造函数
2、创建子类对象时,如果父类对象包含组合对象,那么先调用组合对象的构造函数在调用父类对象的构造函数
委托+继承
用观察者模式做案例(Observer)
Composite:文件系统怎么创建
Prototype(原型): 未来的class名称不知道怎么处理
如果我们男生很喜欢这个女生,想和她在一起,绝大部分给女生的答案一定是肯定的,甚至说得更加夸张,把自己夸的天花乱坠。
程序设计兼谈对象模型
更多细节的深入
operator type() const{}
conversion function 转换函数
默认返回值是函数名中的type,无参数
首先找 有没有重载的+运算符,没有就继续找转换函数
转换函数实际运用
explicit complex(...):initialization list{}
non-explicit-one-argument ctor
找到Fraction对象中有+重载运算符,类型不匹配后,调用non-explicit ctor 将4转为Fraction(4,1)在进行运算
conversion function vs non-explicit-one-argument ctor
如果同时存在转换函数和非明确1个参数的构造函数 二条路,那么编译器会无法选择其中一条路,会报错
explicit-one-argument ctor(一般用到构造函数中)
不能让4直接调用构造函数
pointer-like classes (智能指针,一定带有指针,像指针但是比指针更智能)
1、pointer-like classes,关于智能指针shared_ptr
智能指针肯定有重载*和->() 符号,而->符号可以继续传递下去
2、pointer-like classes,关于迭代器
特别的智能指针,还涉及到++、--等操作
function-like classes 仿函数
函数对象,operator()(const Pair& a){}
namespace 经验谈
放不同的namespace里
class template,类模板
类模板
函数模板
函数模板会自动推导类型,不用定义类型
成员模板
类本身有模板,类里面的函数还有模板
specialization 模板特化
全特化
偏特化
个数上的偏
范围上的偏
上面的T是泛化,下面的T是特化
template template parameter 模板模板参数
其中的list是没指定的,所以是模板的模板参数,但语法不通过是因为容器参数是多个
关于C++标准库
三个主题
variadic templates(since C++11)模板参数可变化
一个和一包,sizeof...(args)就能知道一包有几个
auto(since C++11)
ranged-base for(since C++11)
reference
注意:1、sizeof(r)==sizeof(x) 2、&x = & r object 和 其reference的大小相同,地址也相同(全都是假象) Java里面所有的变量都是reference
reference通常不用于声明变量,而用于参数类型和返回类型的描述
函数签名是返回值后面的表示
Object Model
重讲了一下类之间的关系
红色部分,是编译器会自动加的代码
对象模型:关于vptr和vtbl
继承: 数据继承下来,函数继承的是调用权
举个例子
静态绑定和动态绑定
静态绑定一定是绑定到某个地址,即call xxx
动态绑定有3个条件(多态)
1、通过指针
2、向上转型
3、调用虚函数
对象模型(Object Model):关于this
模板方法(设计模式),体会使用虚函数的好处
CDocument::OnFileOpen(&myDoc);
this->Serialize(); 转换成 (*(this->vptr)[n])(this); //动态绑定
对象模型:关于Dynamic Binding
因为对象a不是指针,所以此处并没有采用动态编译,而是静态编译
上图2种形式是采用了动态编译
关于new,delete
new表达式
介绍下 operator new 和 operator new[]
全局重载
成员函数重载
示例、接口
分配多个空间会多加一个计数器的指针
代码
#include<iostream> using namespace std; class Foo { public: int _id; long _data; string _str; public: Foo() :_id(0) { cout << "default ctor" << this << " id= "<<_id << endl; } Foo(int i) :_id(i) { cout << "ctor.this=" << this << "id = " << _id << endl; } ~Foo() { cout << "dtor.this=" << this << "id=" << _id << endl; } static void* operator new(size_t size); static void operator delete(void* pdead, size_t size); static void* operator new[](size_t size); static void operator delete[](void* pdead, size_t size); }; void* Foo::operator new(size_t size) { Foo* p = (Foo*)malloc(size); cout << "new size=" <<size<< endl; return p; } void Foo::operator delete(void* pdead, size_t size) { cout << "hello delete" << endl; free(pdead); } void* Foo::operator new[](size_t size) { Foo* p = (Foo*)malloc(size); cout << "new[] size" <<size<< endl; return p; } void Foo::operator delete[](void* pdead, size_t size) { cout << "hello delete[]" << endl; free(pdead); } int main() { // cout<<sizeof(string)<<endl; // cout<<sizeof(int)<<endl; // cout<<sizeof(long)<<endl; Foo* f1 = new Foo(7); //cout << sizeof(*f1) << endl; delete f1; Foo* f2 = new Foo[5]; //cout << sizeof(*f2) << endl; delete[] f2; return 0; }
重载new() , delete(),即placement new
示例
placement new 的使用
代码
#include<iostream> #include<malloc.h> using namespace std; class Bad {}; class Foo { public: Foo() { cout << "Foo::Foo()" << endl; } Foo(int i) { cout << "Foo::Foo(int)" << endl; throw Bad(); } ~Foo() { cout << "dtor" << endl; } void* operator new(size_t size) {//1 return malloc(size); } void* operator new(size_t size, void* start) {//2 return start; } void* operator new(size_t size, long extra) {//3 return malloc(size + extra); } void* operator new(size_t size, long extra, char init) {//4 return malloc(size + extra); } //void* operator new(long extra, char init) {//5,第一个参数错误写法 // return malloc(extra); //} void operator delete(void*, size_t) {//1 cout << "operator delete(void*,size_t)" << endl; } void operator delete(void*, void*) {//2 cout << "operator delete(void*, void*) " << endl; } void operator delete(void*, long) {//3 cout << "operator delete(void*, long) " << endl; } void operator delete(void*, long ,char) {//4 cout << "operator delete(void*, long ,char) " << endl; } }; int main() { Foo start; Foo* p1 = new Foo; Foo* p2 = new(&start) Foo; Foo* p3 = new(100) Foo; Foo* p4 = new(100,'a') Foo; Foo* p5 = new(100) Foo(1); Foo* p6 = new(100,'a') Foo(1); Foo* p7 = new(&start) Foo; Foo* p8 = new Foo(1); }
placement new的现实案例
string字符串中,当你创建对象时,使用placement new 可以额外的分配一些空间,如计数
浮动主题