-
1.学习目标
1. 培养正规大气的编程习惯
2. 以良好方式编写C++ class,主要是单一class的设计(object based)
- class without pointer member
- class with pointer member
3. 学习classes之间的关系, 多个classd的设计(object oriented)
- 继承inheritance
- 复合composition
- 委托delegation
2. 关于C++
1. C++演进:C++98--->C++ 03--->C++ 11---> C++ 14--->C++ 17 ---> C++20 --->C++ 23
2. C++ 包含两部分内容: C++语言 + C++标准库
3. C++ class/struct: 封装数据和函数成为对象
4. 基于对象和面向对象
- 基于对象(object based): 面向单一class的设计
- 面向对象(object oriented): 面向多个/重class的设计,侧重于class与class之间的关系
6. class的经典分类
- class with pointer member(s)
- class without pointer member(s)
3. 无指针成员变量的单个类设计complex
3.1 inline函数
类内定义的函数自动成为inline函数,类外定义的函数需要添加inline关键字才成为inline函数。但inline函数只是对编译器的建议,最终是否能成为inline函数依赖于编译器。inline机制使得函数调用处直接采用函数定义代码代替,增加编译时间而减少函数调用运行时间,类似于#difine.
3.2 构造函数ctor
1. 构造函数中优先使用初始化列表,而非赋值方式。
//初始值列表方式,推荐
complex(double r = 0, double i = 0):re(r), im(i){}
//赋值方式
complex(double r = 0, double i = 0){ re = r; im = i;}
2. C++同名函数可以有多个,称为重载overloading,编译器编译后重载函数的名称不同。对于构造函数,可以有多个。
3. C++构造函数中可以设置默认值,但要注意默认值的作用。如下两个构造函数同时定义会出现编译问题:
//同时定义会存在编译问题
complex(double r = 0, i = 0): re(r), im(i){}
complex(): re(0), im(0){}
//使用
complex c1;
complex c2();
4. 构造函数ctor放置在private区域时,不能直接访问对象成员变量值。使用此特性,构造单体模式。使用示例A::getInstance().setup();
//使用示例
A::getInstance().setup();
//C++ Singleton模式示例
class A
{
public:
static A& getInstance();
setup() {...}
private:
//构造函数放置在private区
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
//static静态变量放置在此函数内部,只有真正调用getInstance()时才创建
static A a;
return a;
}
3.3 const关键字
1. 常量成员函数: 成员函数后面带const,表示不改变成员变量值。
class complex
{
public:
double real() const{return re;}
double imag() const{return im;}
private:
double re, im;
};
complex c1(2, 4);
cout << c1.real() << endl;
2. 带const的变量:表示变量值不允许修改。
3. 在不改变成员变量值的成员方法后面加const,是一个良好习惯。否则,使用者调用类成员方法时可能会出错。如下示例:
class complex
{
public:
//如下成员方法定义时没有加const
double real() {return re;}
double imag() {return im;}
...
private:
double re, im;
};
//const变量调用成员方法,编译器报错
const complex c1(2, 4);
cout << c1.real() << endl;
3.4 参数传递pass by value & pass by reference(to const)
1. C++中参数传递尽量传引用,传引用是比传指针更为优雅的方式,
2. 传引用时如果不需要修改参数,前面可以加const。
3.5 返回值传递return by value & return by reference(to const)
1. 在允许的情况下尽量采用返回引用的方式返回参数。
2. 局部变量或临时对象不能采用return by reference的方式返回.
3.6 friend
1. 使用friend函数可以自由获得类私有成员变量值。
2. 同一个类的各个对象互为friends,一个对象可以直接获得另一个对象的成员变量值。
class complex
{
public:
int func(const complex& c)
{
return c.re + c.im;
}
private:
double re, im;
};
{
complex c1(2, 1);
complex c2(1, 0);
c2.func(c1); //c1 和 c2互为friends
}
3.7 this指针
1.C++类内成员函数内部可以使用this指针,因编译器编译成员函数时中默认会添加this指针的参数。不同编译器放置this指针的位置不确定,this可能是第一个参数,也可能是最后一个参数。
class complex
{
public:
complex& operator+=(const complex& r);
...
};
inline complex&
__doapls(complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex&
operator += (const complex& r) //operator += (this, const complex& r), this指针
{
return __doapls(this, r);
}
3.8 操作符重载
1. 作为成员函数的操作符重载函数,即类内操作符重载,作用于对象上,如3.7中例子。此时在操作符重载函数内部可以使用this指针指代当前对象。
2. 非成员函数的操作符重载函数,不包含this指针,此时可以作用于多个对象。
//c1 += c2;
complex operator += (const complex& x, const complex& y)
{
return complex(x.real()+y.real(), x.imag()+y.imag());
}
3.9 临时对象
1. 一种形式的临时对象:typename().
{
complex c1(2, 1);
complex c2;
complex(); //临时对象
complex(4, 5); //临时对象
}
inline
complex operator += (const complex& x, double y)
{
return complex(real(x)+y, imag(x)); //返回临时对象的值
}
4. 带指针成员变量的单个类设计String
//String.h
#ifndef MYSTRING_H
#define MYSTRING_H
#include <cstring>
#include <iostream>
class String
{
public:
String(const char* cstr = 0);
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;
}
inline
String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
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
4.1String类构造函数和析构函数
String类含有一个指针类型的成员变量m_data,该指针指向C风格字符串实际保存处。
采用这种指针的方法,可以存储不同长度的字符串。
如下代码,内存中结构如图所示:
- String *p = new String("hello"); 在栈上申请分配一个String类对象大小的空间,并调用String类的构造函数初始化该空间。其中指针p指向这个空间,该空间内仅存储一个指针变量m_data。
- 调用String类构造函数时,会申请分配堆空间来存储"hello", 并将m_data指向这块堆存储空间。
- String类对象不再使用死亡时,调用析构函数释放存储"hello"的堆空间, 即成员变量m_data指向的空间。局部变量p占用的栈空间会自动释放。
4.2 三大函数
- 拷贝构造函数
- 拷贝赋值函数
- 析构函数dtor
4.2.1 含有指针类型成员变量的类必须要有拷贝构造函数和拷贝赋值函数
copy ctor和copy op=函数确保对象间拷贝是深拷贝,每个对象的指针成员变量都有自己的一份存储空间来存储数据。
如下图,使用默认ctor或op=函数,仅仅是浅拷贝,将会使两个对象的指针成员变量指向同一处存储空间,另一个对象的造成内存泄露。
4.2.2 拷贝赋值函数中必须要判断是否是自我赋值(self assignment)
如下图,当自我赋值拷贝时,*this和rhs对象的m_data指针都指向同一块内存。 赋值拷贝时会首先释放*this和rhs对象指针所指内存, 再访问rhs为*this申请分配内存时出现不确定的行为。
4.3 C++内存布局与对象生命周期
内存布局:
- 栈区stack:存在于某作用域内(如函数)的一块内存空间,存储函数运行时使用的非静态局部变量、函数参数、函数返回值、返回地址等。函数执行完成后,这些变量都将从stack上获自动释放。
- 堆heap:操作系统提供的一块存储空间,程序可以动态申请。一般需要程序员主动释放。
- 数据段:包含静态存储区 + 全局存储区
- 代码段
-
class Complex{...}; //global object, 作用域为整个程序,生命周期直到程序运行结束 Complex c3(1,2); { //c1所占空间来自stack,是stack object,在该作用域后c1被自动清理释放 Complex c1(1, 2); //Complex(3)是个临时对象,其所占空间是由new从heap上动态申请而来,并由c2指向, 该临时对象需要使用delete释放 Complex *c2 = new Complex(3); //static object, 其生命在作用域外仍然存在,直到程序运行结束 static Complex c4(3, 4); delete c2; //释放c2所指的heap空间,c2的生命周期到此结束;否则会有内存泄露 }
4.4 关于new和delete操作符
4.4.1 new操作符原理
new操作可以分为三步:
- 先分配内存,通过operator new来申请一块对象大小的内存(内部调用malloc)。
- 转换内存类型
- 执行构造函数
4.4.2 delete操作符原理
delete操作主要有2步:
- 执行对象的析构函数dtor
- 释放对象所占内存,通过operator delete实现(内部调用free)。
4.4.3 new/delete & new[]/delete[]必须配对使用
1. new和new[]时内存布局
使用new和new[]申请内存,最后都调用malloc()来申请内存。malloc()申请内存的结构如下图所示,包括内存块总大小(红色存储单元,包含两个头尾存储块空间,最后一位标识该块内存是否已经分配 0-未分配/1-已分配)、对象个数、每个对象所占空间。
使用new分配内存示例(32位机器vc环境):
使用new[]分配内存示例(32位机器vc环境):
2. new[]与delete[]搭配使用
如果使用了new String[3]申请了3个String对象的内存空间,而只使用delete p来释放空间,可以看到只会调用一次String的析构函数释放一个String对象中的m_data所申请空间,其他2个String对象中m_data所指向内存不会释放。
即对于含有pointer member的类,如果不正确使用delete[],泄露的是类对象指针成员所指向的动态内存,而不是类对象本身所占用内存。
5. 面向对象的类设计
5.1 类和类之间关系
- 继承inheritance:表示一个类继承自另一个类,两个类是is-a的关系。
- 复合composition: 表示一个类中包含另一个类的对象,两个类是has-a的关系。
- 委托delegation:表示一个类中包含有另一个类的对象指针,即composition by reference。
5.2 复合composition
表示一个类中含有另一个类的对象。用实心菱形箭头指向被包含者表示两者之间的关系。
复合Composition关系下的构造和析构:
构造函数由内到外执行:先执行内部Component的default构造函数,再执行自己的构造操作,代码类似于Container::Container(...):Component(){...};
析构函数执行时由外到内执行,先执行Container的析构函数,再执行Component的析构函数,代码类似于
Container::~Container(...){...; ~Component(); }
5.3 委托Delegation
一个类中含有另一个类的指针,即Composition by reference。用空心菱形箭头指向被包含者表示两者之间关系。
5.4 继承Inheritance
继承表示子类继承自父类,是is-a的关系。用空心箭头从子类指向父类来表示继承关系。
父类的析构函数dtor必须定义成virtual,防止对子类析构时报错。
继承关系下的构造函数执行过程:
构造由内到外:即先执行基类的构造函数,然后再执行自己。代码类似于:
Derived::Derived(): Base(){ ... };
析构由外而内:即先执行子类的析构函数,然后再执行自己的。代码类似于:
Derived::~Derived(...){...; ~Base(); }
5.5 继承与虚函数
Derived类继承Base类时,Base类中函数定义有如下情况:
- non-virtual函数,不希望Derived类来重新写函数来覆盖它。
- virtual函数,希望Derived类重新写函数定义它,并且Base类中该函数已有定义。
- pure virtual函数,Derived类一定要重新定义实现它,并且Base类中没有该函数定义。
5.5.1 TemplateMethod示例
如下,myDoc对象将调用父类CDocument::OnFileOpen()函数,在该函数中传入指针为&myDoc。执行到Serialize()时查询myDoc的虚函数表vfunc table,调用自己的Serialize()。执行完自己Serialize()后返回继续执行OnFileOpen()。
#include <iostream>
using namespace std;
class CDocument
{
public:
void OnFileOpen()
{
cout << "dialog..." << endl;
cout << "check file status..." << endl;
cout << "open file..." << endl;
Serialize();
cout << "close file..." << endl;
cout << "update all views..." << endl;
}
virtual void Serialize() {};
};
class MyDoc: public CDocument
{
public:
virtual void Serialize()
{
cout << "MyDoc::Serialize()..." << endl;
}
};
int main()
{
MyDoc myDoc;
myDoc.OnFileOpen();
return 0;
}
//output:
$ g++ -o TemplateMethod ./TemplateMethod.cpp
$ ./TemplateMethod
dialog...
check file status...
open file...
MyDoc::Serialize()...
close file...
update all views...
5.6 继承与复合(Inheritance+Composition)
继承和复合关系下的构造和析构:
1. 第一种关系如下:
构造时,Devired先调用Base类的default构造函数,然后再调用Component的构造函数,最后执行自己的操作。代码类似于:
Derived::Derived(): Base(), Component() { ... };
析构时,先执行自己的析构,然后再析构Base和Component。代码类似于:
Derived::~Derived{ ...; ~Component(); ~Base() }
2.第二种关系如下:
构造时由内而外,Derived先调用Component的构造函数,再调用Base类构造函数,最后执行自己的构造操作。
析构时由外而内,Derived先执行自己的析构操作,再调用Base类析构函数,最后调用Component类析构函数。
5.7 继承与委托(Inheritance+Delegation)
继承+委托可以使用多态特性,符合面向对象中的“优先使用组合而非继承”原则, 是设计模式中常见方法。
5.7.1 Observer模式
5.7.2 Composite模式
例如文件系统中,文件和目录的表示可以采用Composite模式。此时Primitive类可以代表文件,Composite类可以用来表示目录。
#include <vector>
class Component
{
public:
Component(int val)
{
value = val;
}
virtual void add(Component*) {}
private:
int value;
};
class Primitive: public Component
{
public:
Primitive(int val): Component(val){}
};
class Composite: public Component
{
public:
Composite(int val): Component(val) {}
void add(Component *elem)
{
c.push_back(elme);
}
private:
vector<Component *> c;
};
5.7.3 Prototype模式
#include <iostream>
enum imageType
{
LSAT,
SPOT
};
class Image
{
public:
virtual void draw() = 0;
static Image *findAndClone(imageType);
protected:
virtual imageType returnType() = 0;
virtual Image *clone() = 0;
//As each subclass of Image is declared, it registers its prototype
static void addPrototype(Image *image)
{
_prototypes[_nextSlot++] = image;
}
private:
//addPrototype() saves each registered prototype here
static Image *_prototypes[10];
static int _nextSlot;
};
//Client calls this public static member function when it needs an instance of an Image subclass
Image *Image::findAndClone(imageType type)
{
for(int i = 0; i < _nextSlot; i++)
{
if(_prototypes[i]->returnType() == type)
return _prototypes[i]->clone();
}
}
class LandSatImage:: public Image
{
public:
imageType returnType()
{
return LSAT;
}
void draw()
{
cout << "LandSatImage::draw()" << endl;
}
//when clone() is called, call the one-argument ctor with a dummy arg
Image *clone()
{
return new LandSatImage(1);
}
protected:
//This is only called from clone()
LandSatImage(int dummy)
{
_id = _count++;
}
private:
//Mechanism for initializing an Image subclass - this causes the default ctor to be called, which
//registers the subclass's prototype
static LandSatImage _landSatImage;
//This is only called when the private static data member is inited
LandSatImage()
{
addPrototype(this);
}
//Nominal "state" per instance mechanism
int _id;
static int _count;
};
//Register the subclass's prototype
LandSatImage LandSatImage::_landSatImage;
//Initialize the "state" per instance mechanism
int LandSatImage::_count = 1;
class SpotImage: public Image
{
public:
imageType returnType()
{
return SPOT;
}
void draw()
{
cout << "SpotImage::draw()" << endl;
}
Image *clone()
{
return new SpotImage(1);
}
protected:
SpotImage(int dummy)
{
_id = _count++;
}
private:
static SpotImage _spotImage;
SpotImage()
{
addPrototype(this);
}
int _id;
static int _count;
};
SpotImage SpotImage::_spotImage;
int SpotImage::_count = 1;
//Simulated stream of creation requests
const int NUM_IMAGES = 8;
imageType input[NUM_IMAGES] = {LSAT, LSAT, LSAT, SPOT, LSAT, SPOT, SPOT, LSAT};
int main()
{
Image *images[NUM_IMAGES];
//Given an image type, find the right prototype and return a clone
for(int i = 0; i < NUM_IMAGES; i++)
{
images[i] = Image::findAndClone(input[i]);
}
//Demonstrate that correct image objects have been cloned
for(i = 0; i < NUM_IMAGES; i++)
{
images[i]->draw();
}
//free the dynamic memory
for(i = 0; i < NUM_IMAGES; i++)
{
delete images[i];
}
return 0;
}