候捷-C++面向对象高级开发
笔记参考
一些候捷C++视频比较完善的学习笔记,可以参考学习一下:
学习目标
- 培养正规的、大气的编程习惯
- 以良好的方式编写C++ class (没有指针成员——complex.h示例、有指针成员——string.h示例)——基于对象
- 学习Class之间的关系继承、复合、委托(opp-demo.h示例)——面向对象
complex类
构造函数
使用构造函数的初值列初始化类的成员变量,参考C++构造函数初始化列表与赋值。
适当的做法:
//构造函数
complex(double r = 0, double i = 0)
:re(r),im(i)//初值列,初始列
{ }
不适当的做法:
//构造函数
complex(double r = 0, double i = 0)
{re=r;im=i;}//赋值
注:不带指针的类大部分不需要析构函数。
常量成员函数
在类的成员函数说明后面可以加const关键字,则该成员函数为常量成员函数。常量对象,以及常量对象的引用或指针都只能调用常量成员函数,参考 C++之常量成员函数。
成员函数在不修改变量的情况下尽量加上const:
double real() const {return re;}
double imag() const {return im;}
参数传递
成员函数中参数传递尽量使用引用,参数传递方式主要有以下几种:
- 值传递:不推荐,只有参数内存大小在4个字节以内才会使用,参数传递语法如 (Complex add)
- 引用传递:推荐,相对于传指针但比指针更优雅,参数传递语法如 (Complex& add)
- 常量引用传递:根据需求使用,加了const后引用参数的值不能被函数修改否则编译器会报错,参数传递语法如 (const Complex& add)
函数返回值
成员函数的返回值也尽量使用引用(建立在非局部变量的情况下),函数内申明的局部变量不能使用引用返回。
inline Complex& _doapl(Complex* ths, const Complex& add)
{
ths->re += add.re;
ths->im += add.im;
return *ths;
}
注:使用引用返回值时,传递者无需知道接受者是以引用还是值的方式接受。
临时对象
参考 二十一、C++中的临时对象:
- 直接调用构造函数将产生一个临时对象
- 临时对象的生命周期只有一条语句的时间(过了这条 C++ 语句,临时对象将被析构而不复存在)
- 临时对象的作用域只在一条语句中
//typename()创建一个临时对象
inline Complex operator + (const Complex& x, const Complex& y)
{
return Complex(x.real() + y.real(), x.imag() + y.imag());
}
友元
在类中指定的友元就可以访问该类中受保护的内容,成员函数、类、全局函数都可以做为友元,参考 C++ 友元。
需要注意,相同class的各个objects互为friends(友元)。
string类
三大函数
拷贝构造函数(copy ctor):若自定义的类中有指针则要自己定义拷贝构造函,系统默认的构造函数只会浅拷贝(将指针拷贝)。
//拷贝构造函数
inline
String::String(const String& str)
{
m_data=new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
String s1("hello");
String s2(s1); //直接取另一个object的private data(兄弟之间互为friend)
拷贝赋值函数(copy assignment operator):将原来的空间清空,分配一个和拷贝的对象的一样大的空间,将内容拷贝过去。
//拷贝赋值函数
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;
}
String s2 = s1;
析构函数:析构函数会在此类对象被释放时自动执行,如离开作用域时。
inline
String::~String() //离开作用域,调用析构函数
{
delete[] m_data; //释放掉因创建对象动态分配的内存
}
带指针的类如果使用默认的拷贝构造函数、拷贝赋值函数会产生内存泄漏、别名的严重错误,如下图所示:
堆、栈与内存管理
栈(Stack):存在与某作用域(scope)的一块内存空间(memory space),如当调用函数时,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。
在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述stack,离开作用域的时候会自动释放。
堆(Heap):由操作系统提供的一块 global 内存空间,程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。
当离开作用域{}的时候,动态分配的内存不会消失,即从堆中动态取得的内存不会自动消失,需要手动释放(delete 掉)。
静态对象:一个对象前面加上 static 修饰符后,既变成所谓的静态对象(static object),其生命在作用域(scope)结束之后仍然存在,直到整个程序的结束。
全局对象:定义在任何作用域或者说大括号之外的对象,其生命在整个程序结束之后才结束,也可以把它视为一种static object,其作用域是整个程序。
new这个动作被编译器分解为分配内存、转换类型、调用构造函数三个动作:
Complex *pc;
void* mem = operator new( sizeof(Complex) ); //分配内存
pc = static_cast<Complex*>(mem); //强制类型转换
pc->Complex::Complex(1,2); //构造函数
与new对应的是delete,编译器解释为调用析构函数、释放内存两个动作:
Complex::~Complex(pc); //析构函数
operator delete(pc); //释放内存
如果对动态分配所得的内存块(in VC)感兴趣,可以参考堆、栈与内存管理。
扩展补充:类模板、函数模板及其他
关于类方数据成员、静态成员、函数、静态函数:
- 类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他本类的对象。
- 类的静态数据只有一份,静态数据在类内部声明后需要在类外部定义。
- 成员函数只有一个,但它要处理很多个对象,this会传递进成员函数,this(本质为指针)告诉成员函数在什么时候该处理哪一个对象。
- 静态函数与非静态函数的差别在于静态函数没有this,只能处理静态数据。
如果函数中存在静态变量,只有函数被调用时函数中的静态变量才会开辟地址,如懒汉式单例模式如下:
class A {
public:
static A& getInstance();//getInstance()是对外的唯一窗口
setup(){ ... }
private:
A();
A(const A& ths);
... //相比singleton,这里的static A a;放到了下面的getInstance()中
};
A& A::getInstance()
{ //如果没有人使用getInstance()那a就不存在
static A a;//只有调用getInstance()对象a才会被创建
return a;//离开这个函数对象a还存在并未死亡
}
一个关于类模板(class template)的示例:
template<typename T>//目前T还没有绑定具体数据类型,T只是个符号
class complex
{
public:
complex (T r = 0, T i = 0) : re(r),im(i)
{ ... }
complex& operator += (const complex&);
T real () cosnt { return re; }
T imag () cosnt { return im; }
private:
T re, im;
friend complex& _doapl(complex*, const complex&);
};//谨记勿忘分号
{
complex<double> c1(2.5, 1.5);//用double替换上述全部T
complex<int> c2(2,6);//用int替换上述全部T
...
}
一个关于函数模板(function template)的示例:
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);//函数模板不需要像类模板中明确指出绑定类型,如complex<int> c2(2,6);绑定类型为int.函数模板会进行实参推导
template <class T>
inline
const T& min(const T& a, const T& b)
{
return b < a ? b : a;//编译器不知道如何比较stone b和stone a(要进行操作符重载),设计这个比大小的人,责任不在他身上,而在设计stone的人.
//在C++中的算法全部都是function template
}
继承、复合、委托
Composition(复合):是has-a关系,表示‘我’里面有另外的东西,在UML图中用组合来描述这种关系。
注:UML用实心的菱形+实线箭头来表示组合(Composition),组合表示部分和整体的关系且生命周期是相同的,如:人与手 。
Delegation(委托):也叫Composition by reference(两个类通过引用(指针)复合),是一种有点虚的has-a关系,指针指向的对象可能随时变换,在UML图中用聚合来描述这种关系。
注:UML用空心的菱形+实线箭头来表示 聚合(Aggregation),聚合表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分,如:公司和员工。
Inheritance(继承):是is-a关系,继承方式public、private、protected有3种,最重要的情况就是public,数据是可以完整的继承下来的。
以上三种情况下构造函数、析构函数都遵循构造由内而外、析构由外而内的顺序执行。
虚函数与多态
虚函数、纯虚函数、非虚函数的区别如下:
- virtual(虚)函数:希望devired class(子类)最好去重新定义(overide),且对它(指父类的虚函数)已有默认定义。
- pure virtual(纯虚)函数:希望devired class(子类)一定重新定义(overide),对它(指父类的虚函数)完全没有默认的定义(其实可以由定义,但你不去定义它)。
- non-virtual函数(不是虚函数):希望drived class(子类)不要重新定义(overide,覆盖)。
虚函数、纯虚函数示例如下:
一个非常经典的设计(通过子类对象调用父类函数):
注:myDoc调用父类的OnFileOpen(),myDoc(谁调用我的那个谁)就会成为隐藏的this pointer。从编译器的角度考虑,它会这样写CDocument::OnFileOpen(&myDoc);&myDoc,即myDoc的地址是隐藏的参数,它将被传到父类中的OnFileOpen()函数的参数中。this->Seirialize();通过this 调用Seirialize(),tthis就是&myDoc。