基于对象的程序设计
不带有指针成员变量的类(以复数类Complex为例)
头文件的结构
头文件complex.h的结构如下,主要有4部分:
-
防卫式声明:防止头文件被重复包含
-
前置声明: 声明头文件中用到的类和函数
-
类声明: 声明类的函数和变量,部分简单的函数可以在这一部分加以实现
-
类定义: 实现前面声明的函数
访问级别
类的声明内可以交叉定义多个不同级别的访问控制块:
class complex
{
public:
// public访问控制块1...
private:
// private访问控制块1...
public:
// public访问控制块2...
}
函数设计
内联函数
- 在类声明内定义的函数,自动成为inline函数;
- 在类声明外定义的函数,需要加上inline关键字才能成为inline函数.
inline只是编程者给编译器的一个建议,在编译时未必会真正被编译为inline函数.因此如果函数足够简单,我们就把它声明为inline就好了
构造函数
与其他语言类似,C++的构造函数也可以有默认实参.C++构造函数的特殊之处在于列表初始化(initialization list).
上面两种构造函数的效果是一样的,但是使用列表初始化的效率更高,应尽量使用列表初始化.
常量成员函数
若成员函数中不改变成员变量,应加以 const 修饰
参数的值传递和引用传递
为了提高效率,使用引用传递参数,避免了参数的复制.若不希望在函数体内对输入参数进行修改,应使用const修饰输入参数
函数的参数应尽量使用引用传递.
返回值的值传递和引用传递
为提高效率,若函数的返回值是原本就存在的对象,则应以引用形式返回.
若函数的返回值是临时变量,则只能通过值传递返回.
友元
友元函数不受访问级别的控制,可以自由访问对象的所有成员.
同一类的各个对象互为友元,因此在类定义内可以访问其他对象的私有变量
complex c1, c2;
c2.func(c1); // 因为c1和c2互为友元,因此c2可以在func()函数内调用c1的私有变量
操作符重载
在C++中的操作符重载有两种形式:
- 一种是在类内声明public函数实现操作函重载(这种情况下,操作符是作用在左操作数上的);
- 另一种是在类外声明全局函数实现操作符重载.
为便于调用该类的用户使用,不同的操作符使用不同的方式进行重载。
在类内声明 public 函数重载 +=
输出参数类型为 complex& ,这是为了支持将多个 += 操作符串联起来.若返回参数类型设为 void ,也支持形如 c2+=c1 的运算,但不支持形如 c3+=c2+=c1 的运算.
函数体内调用友元函数 __doapl(complex *, const complex &) 实现功能,其第一个参数接收成员函数内隐含的 this 指针,其内容在函数中会被改变;第二个参数接收重载函数的参数,该参数在函数中不会被改变,以 const 修饰.
在类外声明或函数重载+
考虑到+操作符有三种可能的用法如下:
complex c1(2,1);
complex c2;
c2 = c1 + c2; // 用法1: complex + complex
c2 = c1 + 5; // 用法2: complex + double
c2 = 7 + c1; // 用法3: double + complex
因为重载操作符的成员函数是作用在左操作数上的,若使用类内声明public函数重载操作符的方法,就不能支持第3种用法了.因此使用在类外声明函数重载+运算符.
这3个函数返回的是局部对象(local object),在退出函数时对象就会被销毁,因此不能使用引用传递返回值.
在类外声明函数重载<<
与重载 + 的考虑方法类似, << 操作符通常的使用方式是 cout<<c1 而非 c1<<cout ,因此不能使用成员函数重载 << 运算符.
虑到形如 cout<<c1<<c2<<c3 的级联用法,重载函数的返回值为 ostream& 而非 void.
总结:在编写类的时候应该注意的5件事
在编写类的时候应该注意的5件事,通过这5件事可以看出你写的代码是否大气:
-
构造函数中使用列表初始化(initialization list)为成员变量赋值.
-
常量成员函数使用const修饰.
-
参数的传递尽量考虑使用引用传递,若函数体内不改变传入的参数,应加以const修饰.
-
返回值若非局部变量,其传递尽量考虑使用引用传递,
-
数据放入private中,大部分函数放入public中.
带有指针成员变量的类——以字符串类String为例
类声明如下,使用指针成员变量m_data管理String类中的字符串数据.
3个特殊函数:拷贝构造函数、拷贝赋值函数和析构函数
对于不带有指针的类,这3个函数可以使用编译器默认为我们生成的版本;但是编写带有指针的类时就有必要定义这3个特殊函数.
构造函数和析构函数
构造函数和析构函数中执行数据的深拷贝和释放.
值得注意的是使用delete[]操作符释放数组内存,若直接使用delete操作符释放数组内存虽然能够通过编译,但有可能产生内存泄漏.
拷贝构造函数和拷贝赋值函数
注意拷贝3调用的是拷贝构造函数。
String s1 = "hello";
String s2(s1); // 拷贝1: 调用拷贝构造函数
String s3;
s3 = s1; // 拷贝2: 调用拷贝赋值函数
String s4 = s1; // 拷贝3: 调用拷贝构造函数
拷贝构造函数的实现较为简单,直接调用友元对象的数据指针进行拷贝即可.
拷贝赋值函数中要检测自我赋值,这不仅是为了效率考虑,也是为了防止出现bug.
若在拷贝赋值函数中不检测自我赋值,在第2步中会出现bug.
堆栈与内存管理
https://blog.csdn.net/jialong_chen/article/details/115421050
new和delete过程中的内存分配
- new操作先分配内存,再调用构造函数.
- delete操作先调用析构函数,再释放内存.
VC中对象在debug模式和release模式下的内存分布如下图所示,变量在内存中所占字节数必须被补齐为16的倍数,红色代表cookie保存内存块的大小,其最低位的1和0分别表示内存是否被回收.
Complex对象 |
String对象 |
|
|
数组中的元素是连续的,数组头部4个字节记录了数组长度:
Complex对象 |
String对象 |
|
|
根据数组在内存中的状态,自然可以理解为什么 new[] 和 delete[] 应该配对使用了: delete 操作符仅会调用一次析构函数,而 delete[] 操作符依次对每个元素调用析构函数.对于 String 这样带有指针的类,若将 delete[] 误用为 delete 会引起内存泄漏.
static成员
https://zhuanlan.zhihu.com/p/37439983
静态成员变量:类的所有对象共有。
静态成员函数:只能访问静态成员变量和静态成员函数。
面向对象(Object Oriented)的程序设计——类之间的关系
类之间的关系有复合(composition)、委托(aggregation)和继承(extension)3种.
类之间的关系
复合(composition)
复合表示一种has-a的关系,STL中queue的实现就使用了复合关系.这种结构也被称为adapter模式.
复合关系下构造由内而外,析构由外而内:
委托(aggregation;composition by reference)
使用类指针。
继承(extension)
继承表示一种is-a的关系,STL中_List_node的实现就使用了继承关系.
继承关系下构造由内而外,析构由外而内:
虚函数
成员函数有3种: 非虚函数、虚函数和纯虚函数
-
非虚函數(non-virtual function): 不希望子类重新定义(override)的函数.
-
虚函數(virtual function): 子类可以重新定义(override)的函数,且有默认定义.
-
纯虚函數(pure virtual function): 子类必须重新定义(override)的函数,没有默认定义.
使用虚函数实现框架: 框架的作者想要实现一般的文件处理类,由框架的使用者定义具体的文件处理过程,则可以用虚函数来实现.
面向对象设计范例
使用委托+继承实现Observer模式
使用委托+继承实现Composite模式
使用Composite模式实现多态,类结构图如下
使用委托+继承实现Prototype模式
参考
https://blog.csdn.net/ncepu_Chen/article/details/113843775