- 培养正规的、大气的编程习惯
- 以良好的方式编写C++ class 【Object based(基于对象)】
- class without pointer members
- Complex
- class with pointer members
- String
- class without pointer members
- 学习Classes 之间的关系 【Object Oriented(面向对象)】
- 继承(inheritance)
- 复合(composition)
- 委托(delegation)
-
B语言(1969)
-
C语言(1972)
-
C++ 语言 (1983)
(new C -> C with Class -> C++)
-
Java 语言
-
C# 语言
- C++ 98 (1.0)
- C++ 03 (TR1, Technical Report 1)
- C++ 11 (2.0)
- C++ 14
C++ = C++语言 + C++标准库
推荐书籍:
- 《C++ Primer》
- 《THE C++ PROGRAMMING LANGUAGE》
- 《Effective C++》
- 《The C++ Standard Library》
- 《STL 源码剖析》
- C语言:语言没有关键,数据必须是全局的
- C++:增强版struct;数据和函数包在一起;数据之间不会混杂
- Object based:面向的是单一 class 的设计
- Object Oriented:面对的是多重 classes 的设计,classes 和 classes 之间的关系
Classes 的两个经典分类:
- Class without pointer member(s):Complex
- Class with pointer member(s):String
延伸文件名(extension file name)即文件扩展名不一定是 .h 或 .cpp,也可能是 .hpp 或其它或甚至无延伸名。
2.5 Header(头文件)的布局 2.6 class的声明(declaration) 2.7 class template(模板)简介实部和虚部的类型在使用的时候才确定。
2.8 inline(内联)函数即使写了inline 关键字,但是函数最终是否能成为 inline,由编译器决定。
2.9 access level(访问级别)所有的数据都放在 private,如果函数是要被外界调用的就放在public,若不打算被外界调用则放在 private。
class body内的 public 和 private 可以交错地出现,并不是只能分为两段,比如:
class complex {
private:
...
public:
...
private:
...
public:
...
};
三、构造函数
3.1 constructor(ctor,构造函数)
- 构造函数没有返回值类型;
- 构造函数的函数名必须和类名一致;
- 构造函数的参数可以有默认值;
- 只有构造函数才有初始列表,使用初始列表进行数据的初始化比使用赋值进行值的设定的效率更高;
- 本类中没有指针变量,一般这种没有指针的类多半不需要写析构函数。
- 同名函数编译后的实际名称是不同的,取决于编译器;
- 上图中的②的存在时不行的,会与 ① 的构造函数发生冲突,右侧构造对象的是时候不知道调用哪个
- 构造函数放在 private 区后,是不能被外界直接调用的。
- 单例模式:有将ctors放在private区的需求
- const修饰成员函数,要在如上图的位置;
- 不会改变数据内容的函数加上 const;
- const 对象一定要调用 const 方法。
- 数据所占内存很大(32位机器上大于4字节)的时候,传地址(32位机器上占4字节)的速度更快;而引用在底层就是指针,传引用就相当于传指针;
- 参数传递的时候尽量传递引用;
- 如果传递的参数不希望被函数修改,那么就传 const 引用
- 什么情况下可以 pass by reference
- 什么情况下可以 return by reference
运算结果放到一个不存在的空间(变量)中,则不能返回引用,除此之外,都可以返回引用。
五、操作符重载与临时对象 5.1 operator overloading (操作符重载-1,成员函数)this- 所有的成员函数都有一个隐藏的参数 this,指向调用者。
- _doapl,do assignment plus,标准库的代码
传递者 无需知道 接受者 是以 reference形式 接收
- _doapl 函数返回的是 object(指针指向的内容),是个值,但是接收使用的是 complex& 引用;
- c2 += c1,其中 c1 就是传递者。
- 三个全域函数对应的是三种不同的调用情况:复数 + 复数,复数 + 实数,实数 + 复数。
- 操作符重载,有两种选择:成员函数或者全局函数,但是 << 只能写成全局函数;操作符重载是作用在左边的参数上,成员函数默认有个 this 参数,如果写成成员函数,使用的时候就变成了 c1 << cout,不符合一般的使用习惯,因此 << 写成全局函数;
- ostream& operator<<(ostream& os, const complex& x) 函数中的第一个参数 os 不能用 const,因为数据往 os 里丢数据的时候一直都在改变 os;
- ostream& operator<<(ostream& os, const complex& x) 函数的返回类型不能是 void,因为可能以这种方式使用:cout << c1 << conj(c1),如果返回的是 void,则不能连用 <<。
- 这三个函数得到的结果都是目前不存在的,需要新创建,而且返回后可能被外界使用,而这个新创建的对象返回后需要销毁,所以不能返回引用,而一定要返回值。
- typename(); 用于创建临时对象,比如 complex(x + real(y), imag(y));
{
int(7);
complex c1(2, 1);
complex c2;
complex(); //临时对象
complex(4, 5); //临时对象
//到这里的时候,临时对象的生命到此结束
cout << complex(2);
}
5.6 小结
设计一个 class 要注意的事项:
- 构造函数使用 初始列表
- 考虑函数是否要加 const
- 参数的传递尽量使用引用,考虑是否加 const
- 考虑清楚返回值的类型是值还是引用
- 数据放在 private
- 函数绝大部分放在 public
下面的程序是来自标准库的函数,去除了某些部分。
#ifndef _COMPLEX_H
#define _COMPLEX_H
class complex {
public:
complex(double r = 0, double i = 0) : re(r), im(i) {}
complex& operator+=(const complex&);
//函数内部没有修改成员数据,所以加上const
double real() const { return re; }
double imag() const { return im; }
private:
double re, im;
friend complex& __doapl(complex*, const complex&);
};
inline complex& __doapl(complex* ths, const complex& r) {
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex& complex::operator+=(const complex& r) {
return __doapl(this, r);
}
//不将operator+设计为类的成员函数,就是因为加法的左右值类型是多样变化的
inline complex operator+(const complex& x, const complex& y) { //复数 + 复数
return complex(real(x) + real(y), imag(x) + imag(y)); //返回的是local object,所以返回值类型必须是值
}
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));
}
#include
ostream& operator<<(ostream& os, const complex& x) { //返回值为引用考虑了“连用”
return os << '(' << real(x) << ',' << imag(x) << ')';
}
#endif
七、三大函数:拷贝构造,拷贝赋值,析构
7.1 class的两个经典分类
若类中有指针,如果只是简单的拷贝,那就是指针拷贝,两个指针指向的是同一个空间,所以不能直接拷贝。如果类中有指针,一定要自己写拷贝构造。
7.2 String class 7.3 Big Three, 三个特殊函数- 此处使用 char* m_data,就是在使用的时候才指向字符串,就是动态分配的方式;
- get_c_str() 函数没有修改指针,所以加上 const 。
- class中有指针,多半就要进行动态分配;做了动态分配,在的对象死亡后,就要释放掉分配的空间;
- s1、s2、p这三个字符串都会调用String(const char* str = 0) 这个构造函数,离开作用域之后都会调用析构函数。
使用 default copy ctor 或 default op=是浅拷贝,是需要避免的。
7.3.3 copy ctor(拷贝构造函数)此处的
String s2(s1); String s2 = s1;
意思是一样的,都会调用拷贝构造函数。拷贝构造函数里是深拷贝。
7.3.4 copy assignment operator(拷贝赋值函数)①②③的流程:
- 一定要在 operator= 中检查是否 selft assignment
如果不做 self assignment,就会造成如下问题:
检查自我赋值(self assignment)不只是为了效率,还是为了正确性。
7.3.5 output函数operator<< 不能写成成员函数,否则cout和要输出的内容就会相反。
八、堆、栈与内存管理 8.1 所谓 stack(栈),所谓 heap(堆) 8.2 stack objects的生命期「自动」清理即析构函数会被自动调用。
8.3 static local objects的生命期class Complex { ... };
...
{
static Complex c2(1,2);
}
c2 便是所谓 static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。
8.4 global objects的生命期class Complex { ... };
...
Complex c3(1,2);
int main()
{
...
}
c3 便是所谓 global object,其生命在整个程序结束之后才结束。你也可以把它视为一种 static object,其作用域是「整个程序」。
8.5 heap objects的生命周期 8.6 new:先分配memory,再调用ctor- pc 是分配的内存的起始位置,也就是 this。
- ①将字符串里面动态分配的内存销毁;至于字符串本身,只是一个指针;
- ②销毁字符串本身,本身是个指针。
图中为内存结构,在VC中,分配的内存空间大小都是16的倍数,第一个图中是调试模式下, 每个格表示4Bytes,Complex对象占8个字节,计算得到的结果是52Bytes,不是16的倍数,所以补充12个Bytes的填补字节,凑到64。开头和结尾的00000041,其中的 4 是 64的16进制,1表示这块空间已经给出去了,0表示这块空间空闲。第二个图为非调试模式,对象所占的内存空间。
8.11 动态分配的array 8.12 array new 一定要搭配 array delete错误用法delete p;导致出现的内存泄漏是String 类中的指针指向的空间,而非指针本身。
如果是Complex 数组对象,因为其中没有指针,所以就算是使用 delete p; 也是没有问题的。
但是为了万无一失,如果使用了array new 一定要搭配 array delete。
九、复习String类的实现过程
#ifndef _STRING_H
#define _STRING_H
class String {
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
//函数没有更改m_data,所以加上const
char* get_c_str() const { return m_data; }
private:
//动态分配的方式保存字符串
char* m_data;
};
#include
//建议编译器将函数作为内联函数
inline
String::String(const char* cstr = 0) {
if (cstr) {
m_data = new char[strlen(cstr) + 1];
strcpy(m_data, cstr);
} else { //未指定初值
m_data = new char[1];
*m_data = ' ';
}
}
inline
String::~String() {
//array new搭配array delete
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) { //Type&,此处的&表示引用
//考虑自我赋值,必须做
if (this == &str) //&obj,此处的&表示取地址,得到obj的指针
return *this;
//先将本身的东西清除,再分配足够多的空间容纳新的字符串
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this; //考虑到连用,所以要返回引用
}
#endif
十、扩展补充:类模板、函数模板及其它
10.1 进一步补充:static
- 非static 函数通过传入的 this 指针找到要处理的对象;
- static数据只有一份;
- static 函数没有 this 指针;只能处理静态数据。
- 静态数据要在类外进行定义。
创建出来的类只能有一个对象 。
- 第一种写法:
缺点:如果不需要A的实例,但是也创建了。
- 第二种写法:
函数中的 static 对象,只有当调用了该函数的时候,该对象才会创建;该函数执行后,这个static对象仍然存在。这种写法更好。
10.3 进一步补充:cout 10.4 进一步补充:class template,类模板- template和typename 都是关键字,template
告诉编译器当前类型还没有绑定; - 用法:complex
,编译器会将代码中的 T 替换为 double; - 不同的绑定类型会生成不同的代码,模板会造成代码的膨胀,但是这并不是缺点,而是必要的。
- 此处的template
中的 class 和 typename 是一样的; - 使用模板函数的时候不用指出类型,因为编译器会进行引数推导。
Object Oriented Programming,Object Oriented Design
OOP,OOD
- Inheritance(继承)
- Composition(复合)
- Delegation(委托)
- queue 中有一个 deque
类型的c; - queue 是容器,它容纳了 deque;
- deque 中可能有很多接口,但是queue 只开放这几个,queue 的所有功能都是通过 deque 来完成的,这种模式叫做 Adapter 模式;
- 该例子是个特例,并不是所有的Composition 都是这样;
- 有了Container就有 Componenet,生命是一起的;
- 复合就是一种结构中包含其他类型/结构。
内存角度解释Composition:
- queue 中的 c 所占的空间为 40Bytes;
- 后面的图为deque 内部以及 Itr 类内部的数据,明白地显示了为什么c 的大小为 40Bytes。
- 左边拥有右边,左边叫做 Container,右边叫做 Component;
- 红色的部分是编译器自动加上的,并非程序员写的;
- 可能内部的 Component 有多个构造函数,但是编译器不知道需要哪个,所以就调用一个默认的构造函数,如果这并不是你期望的,那就需要自己在Comtainer 的构造函数的初始列位置写上要调用的构造函数。
- 有一个指针指向右边,需要右边的时候才创建右边;
- Delegation 又叫 Composition by reference,因为也是拥有,只是拥有的是个指针;
- 学术界不说by pointer,即使用的是指针传也叫 by reference;
- 两个类之间用指针相连,就叫做 Delegation;
- 因为需要右边的时候才创建右边,所以左右两边的生命不同步;
- String类中的所有操作都是通过rep 指针来实现的,这种方式叫做 pointer to implementation, 指针指向为String 实现所有功能的类;这种设计的好处是,右边无论怎么变化都不影响左边,这种手法又叫做“编译防火墙”;
- 另一个名称叫做 Handle/Body,左边是 Handle,右边是 Body;
- 这种做法是为了做 reference counting,三个 String 对象 a、b和c,都在用字符串"Hello",三个对象共享 “Hello”,现在 n 是 3。内容一样才能共享。如果a想修改字符串的内容,但是不影响 b 和 c,那就拷贝一份让 a 去更改,然后就只剩下 b和 c共享字符串。这是copy-on-write,就是写的时候copy一份副本让你去写。
- 关系图中的 T 表示这是个模板类;
- 当前的例子中子类_List_node 中的数据成员是 _M_data ,但是它还涵盖了 _M_next 和 _M_prev 成员
- 子类的对象中有父类的成分;
- 父类的析构函数必须是 virtual 的,否则会出现 undefined behavior;
- 红色部分是编译器添加的;谁先谁后已经是编译器完成了,程序员不用操心。
- 虚函数被重新定义才能叫做override。
- 输入文件名称后,按下开始键后,程序会收到一个文件名;
- 程序要检测file name是否正确,是否有不合法字符;
- 到硬盘中查找file是否存在;
- 如果存在,则将该文件打开,并读出来。
上面的这个流程所有的软件都是一样的,除了最后将文件读出来有所区别。那么,将这个框架写成模板,即Cdocument类:
CMyDoc myDoc; myDoc.OnFileOpen(); //通过子类调用父类的函数
子类中没有函数onFileOpen(),myDoc.onFileOpen() 的全名是 Cdocument::onFileOpen(&myDoc),所以才能找到 Cdocument类中的OnFileOpen 函数。
Cdocument::onFileOpen(&myDoc); 中传入了 myDoc 的地址,调用Serialize 的时候,编译器通过this 进行调用,即this->Serialize(),而this就是myDoc, 也就调用了CMyDoc 类的Serialized() 函数。
myDoc.onFileOpen()的执行流程:
- 调用Cdocument的OnFileOpen函数;
- 在OnFileOpen 函数里调用了CMyDoc的 Serialize() 函数;
- 执行完了Seriazlize() 函数后又回到onFileOpen() 函数中继续执行;
- OnFileOpen 函数执行完毕后,回到main 函数中。
过程如图中的灰色线所示。
将关键动作 Serialize 延缓到了子类中执行。将onFileOpen() 这个函数的这种做法叫做Template Method模式。
模拟该过程的代码:
11.7 Inheritance+Composition 关系下的构造和析构//自己编写 #includeusing namespace std; class Component { public: Component() { cout << "Component constructor" << endl; } bool test() { return true; } ~Component() { cout << "Component destructor" << endl; } }; class base { public: base() { cout << "base constructor" << endl; } ~base() { cout << "base destructor" << endl; } }; class Derived : public base { protected: Component component; public: Derived() { cout << "Derived constructor" << endl; } bool test() { return component.test(); } ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Derived obj; return 0; }
运行结果:
base constructor Component constructor Derived constructor Derived destructor Component destructor base destructor
Derived 的构造函数先调用 Component 的 default 构造函数,然后调用 base 的 default 构造函数,然后才执行自己。
Derived::Derived(...): Component(), base() {...}
Derived 的析构函数首先执行自己,然后调用 base 的析构函数,然后调用 Component 的析构函数。
Derived::~Derived(...) {... ~base(), ~Component() }
示例程序:
//自己编写 #includeusing namespace std; class Component { public: Component() { cout << "Component constructor" << endl; } void test() { cout << "Component test function" << endl; } ~Component() { cout << "Component destructor" << endl; } }; class base { public: Component comp; base() { cout << "base constructor" << endl; } void test() { comp.test(); } ~base() { cout << "base destructor" << endl; } }; class Derived : public base { public: Derived() { cout << "Derived constructor" << endl; } ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Derived obj; return 0; }
运行结果:
maureen@localhost 03.inheritance_and_composition % ./a.out Component constructor base constructor Derived constructor Derived destructor base destructor Component destructor11.8 Delegation(委托) + Inheritance(继承)
需要解决的问题:
- 四个窗口在看同一份文件/或者同一份数据,三种不同的view查看;
- 当某个窗口的数据发生变化时,其它的也要跟着改变,因为只有一份数据
设计方案:
11.9 委托相关设计- 如何设计一个FileSystem或者WindowSystem?
通常使用 Composite 这种设计模式:
- 创建未来可能出现的类的对象
通常使用 Prototype 模式,子类创建自己类型的原型(对象),并将该对象放到父类中,使得父类能够看见;且子类中都要写函数clone,用于 new 自己。
源码:
相关设计模式来源《Design Patterns Explained Simply》
源码:https://sourcemaking.com/design_patterns/prototype/cpp/3
#includeenum 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; }; Image *Image::_prototypes[]; int Image::_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(); return NULL; } class LandSatImage: public Image { public: imageType returnType() { return LSAT; } void draw() { std::cout << "LandSatImage::draw " << _id << std::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 initiated 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() { std::cout << "SpotImage::draw " << _id << std::endl; } Image *clone() { return new SpotImage(1); } protected: SpotImage(int dummy) { _id = _count++; } private: SpotImage() { addPrototype(this); } static SpotImage _spotImage; 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 (int i = 0; i < NUM_IMAGES; i++) images[i]->draw(); // Free the dynamic memory for (int i = 0; i < NUM_IMAGES; i++) delete images[i]; }



