侯捷--C++面向对象程序设计笔记

c++面向对象程序设计

学习笔记,仅供参考,图片来源于网络

文章目录

一、头文件与类的声明

1、头文件声明

#include <iostream.h> // 引入标准库
#include "complex.h" // 引入自己定义
//延伸文件名(extension file name) 不一定是.h 或.cpp,也可能是.hpp 或其他或甚至無延伸名。

2、c++输出格式

#include <iostream.h> // 引入输出库
using namespace std;
int main()
{
	int i = 7;
	cout << “i=“ << i << endl;
	return 0;
}

3、头文件写法

complex.h为例,写头文件,用于防卫式声明

#ifndef __COMPLEX__ //如果未声明__COMPLEX__,则往下执行define语句,到endif结束,__COMPLEX__名称可以自己改变
#define __COMPLEX__

.....//包含三部分:0:前卫声明、1:类的声明、2:类的定义
    
#endif

对于第1部分:类的声明,以如下为例:

class complex //任何类需要一个class head类头
{ //往下开始写class body
	public:
    	complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义
		complex& operator += (const complex&); // 仅在此声明
		double real () const { return re; } // body内直接定义
		double imag () const { return im; } // body内直接定义
	private:
		double re, im;
		friend complex& __doapl (complex*, const complex&);
};

注:有些函數在class body内直接定义,另一些在body 之外定义

对于第2部分:类的定义,包含许多函数,如下:

inline double imag(const complex& x){
	return x.imag ();
}
inline double real(const complex& x){
	return x.real ();
}
...

最后实例化

{
	complex c1(2,1);
	complex c2;
	...
}

为了避免重复,引入template (模板),类的声明改为:

template<typename T>
class complex
{ 
    public:
		complex (T r = 0, T i = 0): re (r), im (i){ }
		complex& operator += (const complex&);
		T real () const { return re; }
		T imag () const { return im; }
private:
		T re, im;
		friend complex& __doapl (complex*, const complex&);
};

最后实例化

{
	complex<double> c1(2.5,1.5);//这里为类模板使用时需要进行类型的指定,函数模板使用不需要指定类型
	complex<int> c2(2,6);
	...
}

4、inline——内联函数

函数在class body本体内定义,就形成一个inline候选人,用 inline 关键字较好地解决了函数调用开销的问题,能否真正成为inline函数由编译器决定。

complex.h为例,

class complex //任何类需要一个class head类头
{ //往下开始写class body
	public:
    	complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
		complex& operator += (const complex&); // 仅在此声明
		double real () const { return re; } // body内直接定义,inline函数候选人
		double imag () const { return im; } // body内直接定义,inline函数候选人
	private:
		double re, im;
		friend complex& __doapl (complex*, const complex&);
};

在函数前加个inline关键字会成为候选人,太过复杂的函数也不能成为真正inline函数,如:

inline double imag(const complex& x) // inline 函数返回值类型 函数名()
{
	return x.imag ();
}

二、构造函数

complex.h为例,

class complex //任何类需要一个class head类头
{ //往下开始写class body
	public:
    	complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
		complex& operator += (const complex&); // 仅在此声明
		double real () const { return re; } // body内直接定义,inline函数候选人
		double imag () const { return im; } // body内直接定义,inline函数候选人
	private:
		double re, im;
		friend complex& __doapl (complex*, const complex&);
};

complex (double r = 0, double i = 0): re (r), im (i){ }为构造函数,

1、构造函数要点:

  1. 函数名与类名相同
  2. 可以设置参数(该参数可以有默认值),如:(double r = 0, double i = 0)
  3. 没有返回类型,不需要

构造函数特殊初始化语法: re (r), im (i)

complex (double r = 0, double i = 0): re (r), im (i){ }//推荐这种
/*等价于
complex (double r = 0, double i = 0){
	re = r;
	im = i;
}
*/

一个变量的赋值使用有两个阶段:1.初始化,2.赋值,使用。如果不用构造函数的特殊写法,就相当于跳过了初始化,直接在函数中赋值了,效率差。

与构造函数对应的为析构函数。不带指针的类,大多不用写析构函数。

2、函数可以有很多个——overloading(重载)

double real () const { return re; }
void real(double r) const { re = r; }

这两者函数名相同,但返回值类型,形参不同,在编译器看来是两个函数

对于下列的构造函数

complex (double r = 0, double i = 0): re (r), im (i){ }
complex () : re(0), im(0) { }

这样是不行的,因为这两个构造函数在没有形参的情况下,表达意思相同,如complex c1既可以用构造1,也可以用构造2,编译器无法选择。

3、构造函数访问级别

构造函数一般放在public中,放在private中将无法被外界调用用于构造。

对于特殊情况

Singleton(单例模式)

独一份的,指此数据类型只能创建一份。此时是通过调用A类的函数,从而间接的创建对象

class A {
	public:
		static A& getInstance();
		setup() { ... }
	private:
		A();// 构造函数 1
		A(const A& rhs);// 构造函数 2
		...
};
A& A::getInstance()
{
	static A a;
	return a;
}

4、常量成员函数const member functions

对于不会改变数据成员内容的函数,要加上const(在函数小括号后面,大括号的前面),这里就相当于告诉编译器,这是不会变的。

class complex //任何类需要一个class head类头
{ //往下开始写class body
	public:
    	complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
		complex& operator += (const complex&); // 仅在此声明
		double real () const { return re; } // body内直接定义,inline函数候选人,加上const
		double imag () const { return im; } // body内直接定义,inline函数候选人,加上const
	private:
		double re, im;
		friend complex& __doapl (complex*, const complex&);
};

当产生实例方式如以下,方法一:

{
	complex c1(2,1);
	cout << c1.real();
	cout << c1.imag();
}

无特殊变化,正确的

而对于方法二,(尤其是你的定义函数real()imag()const关键字):

{
	const complex c1(2,1);// 加上const
	cout << c1.real();
	cout << c1.imag();
}

会报错,编译器就会理解为这里是可能会变的,这就与const complex造成了矛盾,报错

三、参数传递

1、有三种参数传递方式:

  1. pass by value,传值,例如complex (double r = 0, double i = 0): re (r), im (i){ }
  2. pass by reference,传引用,例如operator << (ostream& os, const complex& x){}
  3. pass by reference to const,传引用const,例如complex& operator += (const complex&);

pass by value会将参数值整个传进去,为了提高效率,尽可能使用pass by reference引用传递参数, 可以避免对参数的复制。

如果在函数内不想对传递变量进行修改,那么就要传引用const。

返回值传递方式同上,尽量使用by reference形式返回,如果要返回的值只是这个函数中的一个局部变量/临时变量,那就一定要用传值来返回。

什么情况下可以pass or return by reference?

  • 当你要return or pass 的值,在调用该函数之前就存在空间来存储,那么就可以使用by reference
  • 当你要return or pass 的值,是在函数内部新声明出来的(局部变量/临时变量),那么不可以使用

2、友元friend

被设置成友元的函数,就可以直接调取私有数据,这比通过公有函数效率更高一点。

class complex //任何类需要一个class head类头
{ //往下开始写class body
	public:
    	complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
		complex& operator += (const complex&); // 仅在此声明
		double real () const { return re; } // body内直接定义,inline函数候选人,加上const
		double imag () const { return im; } // body内直接定义,inline函数候选人,加上const
	private:
		double re, im;
		friend complex& __doapl (complex*, const complex&);
};

friend complex& __doapl (complex*, const complex&)函数中,可以直接调用reim的数据,无需通过real ()imag (),如下:

inline complex& __doapl (complex* ths, const complex& r){
	ths->re += r.re;
	ths->im += r.im;
	return *ths;
}

相同class的各个对象互为友元,这句话就可以解释下面这个用法为何是成立的。也可以说,在类定义内可以访问其他对象的私有变量。

class complex{ 
    public:
		complex (double r = 0, double i = 0): re (r), im (i){ }
		int func(const complex& param){ return param.re + param.im; }
	private:
		double re, im;
};

// 调用
{
	complex c1(2,1);
	complex c2;
	c2.func(c1); //c2可以直接使用c1的re,im
}

四、操作符重载与临时对象

1、对于成员函数

所有的成员函数(声明在class内部的)一定带着一个隐藏参数this指针, 这个this(指针)指向调用这个函数的调用者。代码中参数部分不可以写这个this,但是在函数内部可以用this。传送者无需知道接收者是以什么形式接受

例如:

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);
}
// 调用
{
	complex c1(2,1);
	complex c2(5);
	c2 += c1;// 对于+=操作符,会把c2的地址传到this指针中
    c3 += c2 += c1; // 对于上图中的complex::operator += ,其返回类型设置为complex&,是为了可以处理连续赋值的情况:若不处理连续的+=,可以将返回值设置为void
}

2、对于非成员函数

对全局函数重载:没有this情况下:

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));
}

// 为了应付三种加法,这里要写三种函数。注意这里的返回值一定不能用引用,因为return的都是临时变量。
// 形如typename(),为临时对象,在下一行失效,如:complex(a,b);

//当只有一个参数的时候:这里表示正负号
inline complex operator + (const complex& x){ // 这里应该可以return by reference
	return x;
}
inline complex operator - (const complex& x){
	return complex (-real (x), -imag (x));
}

对于特殊的操作符<<,要把操作符重载设计成全局函数。因为ostream可能是很早之前就定义好的,我们无法提前知道他要输出的类型,不可能预先在成员函数中定义。

#include <iostream.h>
ostream& operator << (ostream& os, const complex& x)
{
	return os << '(' << real (x) << ','<< imag (x) << ')';// 这里的输出在改变os的状态,所以参数上不能加const
}
// 这里的operator << 的返回类型也是考虑到连续输出,所以返回ostream&

五、复习complex类实现过程

1、防卫式定义

#ifndef __COMPLEX__
#define __COMPLEX__

#endif

其中__COMPLEX__为自定义名称

2、定义class head

#ifndef __COMPLEX__
#define __COMPLEX__
class complex
{
    
};
#endif

3、确定私有变量

#ifndef __COMPLEX__
#define __COMPLEX__
class complex
{
	private:
		double re, im;
};
#endif

4、确定构造函数

#ifndef __COMPLEX__
#define __COMPLEX__
class complex{
	public:
		complex (double r = 0, double i = 0): re (r), im (i){ }
	private:
		double re, im;
};
#endif

5、考虑具体函数实现功能

在考虑其他函数(以实现特定功能)时,注意这里可以是成员函数或全局函数(根据需要加inline),思考要不要给函数加const,是否要在private加入友元函数,考虑好函数的接口(参数,返回值的类型),再考虑定义

#ifndef __COMPLEX__
#define __COMPLEX__
class complex{
	public:
		complex (double r = 0, double i = 0): re (r), im (i){ }
		complex& operator += (const complex&);
		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);
}
// 全局函数
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));
}
#endif
#include <iostream.h>
ostream& operator << (ostream& os,const complex& x){
	return os << '(' << real (x) << ','<< imag (x) << ')';
}

六、实现对有指针成员的类string

//string.h 防卫式声明
#ifndef __MYSTRING__
#define __MYSTRING__
class String{ 
    ...
};
String::function(...) ...
Global-function(...) ...
#endif
//string-test.cpp
int main(){
	String s1(),
	String s2("hello");
    
	String s3(s1);// 实现了拷贝构造
	cout << s3 << endl;
	s3 = s2; // 实现了拷贝赋值
	cout << s3 << endl;
}

最后呈现的string类的class body

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;
};

七、三大函数

1、析构函数

形如~类名()为析构函数,与构造函数相对应

以string类为例,如下:

inline String::String(const char* cstr = 0){// string类中的一种构造函数
	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(){ // string类中的析构函数
	delete[] m_data;// delete[]要与new[]相互搭配
}
{
	String s1(),
	String s2("hello");//当s1,s2离开作用域时,析构函数会被调用
	String* p = new String("hello");
	delete p;// 释放字符串p
}

必须要有拷贝构造和拷贝赋值的原因:

String a("Hello");
String b("World");
此时则有两个指针,a-> H e l l o \0 , b -> w o r l d \0
执行b = a;
若不写拷贝构造和拷贝赋值,则会采用系统默认的拷贝,最后结果:
a-> H e l l o \0
b-> H e l l o \0
a和b同时指向H e l l o \0,此时,w o r l d \0没有指针指向,无法调用访问,就产生了内存泄漏
这种也叫做浅拷贝,只拷贝了指针

2、拷贝构造

也为深拷贝,拷贝内容

inline String::String(const String& str){ //函数名与类名相同,收到的参数是自己的类
	m_data = new char[ strlen(str.m_data) + 1 ]; //直接取另一個object 的private data.兄弟之間互為friend
	strcpy(m_data, str.m_data);
}
{
	String s1("hello ");
	String s2(s1); // String s2 = s1;
}

3、拷贝赋值

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 s1("hello ");
	String s2(s1);
	s2 = s1;
}

分为四步:

  1. 检测自我赋值(对于自己给自己赋值的情况,如果不加这一步就会出错)
  2. 删除原有的指针成员的全部内存 (主要是删除了这一步)
  3. 重新给指针成员分配内存大小,和要赋值的内容一样
  4. 赋值

八、堆heap,栈stack与内存管理

栈:是存在于某作用域(scope)的一块内存空间。例如当你调用函数,函数本身就会形成一个stack用来放置它所接收的参数以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都取自上述stack。

堆:是指由操作系统提供的一块global内存空间,程序可动态分配从其中获得若干区块。

class Complex{...};
...
{
    Complex c1(1,2);  //c1所占的空间来自栈,当程序离开该作用域时,生命自动消失
    Complex* p = new Complex(3);  //Complex(3)是临时对象,其占用的空间是以new自堆动态分配而得,并由p指向,需要手动释放
}

1、stack objects的生命期

class Complex { ... };
...
{
	Complex c1(1,2);
}

c1就是stack object, 生命在作用域结束之后结束,这种作用域内的object又称为auto object,因为能被自动清除

2、static local objects 的生命期

class Complex { ... };
...
{
	static Complex c2(1,2);
}

c2就是static object, 生命在作用域(大括号范围)结束之后仍然存在,直到整个程序结束

3、global objects 的生命期

class Complex { … };
...
Complex c3(1,2);
int main(){
	...
}

c3为global object, 生命在整个程序结束后才结束,也可以视为一种static object,作用域是整个程序

4、heap objects 的生命期

class Complex { … };
...
{
	Complex* p = new Complex;
	...
	delete p;
}

p指向heap object, 生命在它被delete之后结束。

如退出作用域时,没有delete,那p指针指向的区域仍存在,而p指针已经死亡,导致内存泄漏

5、使用new和delete时的内存分配过程

new先分配内存再调用构造函数

Complex* pc = new Complex(1,2);

//编译器转化为
Complex *pc;
void* mem = operator new( sizeof(Complex) ); //分配內存,其內部調用malloc(n)
pc = static_cast<Complex*>(mem); //转型
pc->Complex::Complex(1,2); //构造函數

delete先调用析构函数再释放内存

Complex* pc = new Complex(1,2);
...
delete pc;

//编译器转化为
Complex::~Complex(pc); // 析构函數
operator delete(pc); // 释放內存,其內部調用free(pc)

九、复习string类实现过程

1、定义class head 以及防卫式声明

#ifndef __MYSTRING__
#define __MYSTRING__
class String
{

};
#endif

2、定义私有变量

#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
	private:
		char* m_data;
};
#endif

3、确定构造函数

#ifndef __MYSTRING__
#define __MYSTRING__
class String{
    public:                                 
   		String(const char* cstr=0);                     
	private:
		char* m_data;
};
#endif

4、确定三大函数

#ifndef __MYSTRING__
#define __MYSTRING__
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;
};
#endif

5、补充函数实现功能

#ifndef __MYSTRING__
#define __MYSTRING__
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;
};

#include <cstring>
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::operator=(const String& str){ // 拷贝赋值,string& 表示传引用
   if (this == &str) return *this; //检测自我赋值, &str表示得到str的地址,是一个指针
   delete[] m_data; // 删除原有空间
   m_data = new char[ strlen(str.m_data) + 1 ]; // 开辟现需内存
   strcpy(m_data, str.m_data); // 赋值
   return *this;
}

inline String::String(const String& str){ //拷贝构造
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
}

#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str){
   os << str.get_c_str();
   return os;
}
#endif

十、补充

1、static

在变量或函数前添加static关键字,成为静态变量或静态函数

// 在没有static关键字时,complex类有data members 和 member functions
complex c1,c2,c3;
cout << c1.real(); // cout << complex::real(&c1); 这里this指针指向&c1
cout << c2.real(); // cout << complex::real(&c2); 成员函数real()只有一个,是通过this指针,指向谁,来处理谁,这里this指针指向&c2
// 所以完整的real()写法,如下
class complex
{
	public:
		double real () const { return this->re; }// 这里的this可写可不写
	private:
		double re, im;
};

当添加static关键字后,static member data或 static member function就会与对象脱离,在内存有一块单独的区域

static member function没有this指针,所以无法像一般的member function 去处理对象,只能去处理static member data

以银行账户为例

class Account {
	public:
		static double m_rate; //只是声明,静态变量
		static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0; //赋初值定义,必须在类的外头
/*
调用static 函數的方式有二:
(1) 通过object 调用
(2) 通过class name 调用
*/
int main() {
	Account::set_rate(5.0);//通过class name 调用
	Account a;
	a.set_rate(7.0); //通过object 调用
}

2、构造函数放private

在**Singleton(单例模式)**中,用static关键字表明,该类独一份

class A {
	public:
		static A& getInstance( return a; );//静态函数,取得唯一的对象
		setup() { ... } //A::getInstance().setup();通过这样获取setup函数
	private:
		A();//构造函数
		A(const A& rhs);//构造函数
		static A a;// static表明独一份
		...
};

进一步优化,当需要对象A时在创建

class A {
	public:
		static A& getInstance();
		setup() { ... }
	private:
		A();
		A(const A& rhs);
	...
};
A& A::getInstance(){//只有别人调用这个函数时,才会生成静态变量a,且只有一份
	static A a;
	return a;
}

3、模板 template

类模板class template

template<typename T> class complex{ 
	public:
		complex (T r = 0, T i = 0): re (r), im (i){ }
		complex& operator += (const complex&);
		T real () const { return re; }
		T imag () const { return im; }
	private:
		T re, im;
		friend complex& __doapl (complex*, const complex&);
};
//使用
{
	complex<double> c1(2.5, 1.5);// 明确指出typename是什么
	complex<int> c2(2, 6);
	...
}

函数模板function template

template <class T> inline const T& min(const T& a, const T& b){
	return b < a ? b : a;
}
//此时我有一个stone类,如下:
class stone{ 
    public:
		stone(int w, int h, int we): _w(w), _h(h), _weight(we){ }
		bool operator< (const stone& rhs) const { return _weight < rhs._weight; }
	private:
		int _w, _h, _weight;
};
//进行min比较
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);// 编译器会对function template 进行引数推导(argument deduction),引数推导的結果,T为stone,于是调用stone::operator<

4、namespace

语法:关键字+名称

namespace std//std为包裹标准库所有东西的空间
{
	...//这里内容全部被包裹在一个命名空间里面
}

使用方法:

  • 使用命令using directive,全部开放标准库

  • #include <iostream.h>
    using namespace std;
    int main(){
    	cin << ...;
    	cout << ...;
    	return 0;
    }
    
  • 使用声明using declaration,只开放部分标准库

  • #include <iostream.h>
    using std::cout;//声明了cout
    int main(){
    	std::cin << ...;//对于未声明的,只能写完整全名
    	cout << ...;//可以不写全名
    	return 0;
    }
    
  • 未声明

十一、组合与继承

面向对象类和类的关系:

  • Inheritance (继承)
  • Composition (复合)
  • Delegation (委托)

1、Composition (复合),表示has-a

以下列为示例,queue类中拥有 deque类,左边(Container) 拥有右边(Component),Container和Component两者生命是一起出现的。

在这里插入图片描述

template <class T> class queue {//队列
	...
	protected :
		deque<T> c;     //底层容器
	public:
		// 以下完全利用 c 的操作函数完成
		bool empty() const { return c.empty(); }
		size_type size() const { return c.size(); }
		reference front() { return c.front(); }
		reference back() { return c.back(); }
		//
		void push(const value_type& x) { c.push_back(x); }
		void pop() { c.pop_front(); }
}

构造函数来看,

由内而外:外部的 Container的构造函数首先调用内部的Component的default构造函数,然后才执行自己。(红色部分编译器自动加的,使用构造函数特殊语法,当默认构造函数不满足需求时,要自己写)

在这里插入图片描述

析构函数来看,

由外而内: 外部的Container的析构函数首先执行自己,然后才调用内部的Component的析构函数。

在这里插入图片描述

2、Delegation (委托),Composition by reference

左边(Handle)(拥有指针)指向右边(Body),两者以指针相连,不是同时存在的,只有左边用到右边时,右边才存在。

在这里插入图片描述

左边handle

// file String.hpp
class StringRep;
class String {
public:
	String();
	String(const char* s);
	String(const String& s);
	String &operator=(const String& s);
	~String();
. . . .
private:
	StringRep* rep; // pimpl,指向右边的指针
};

右边body

// file String.cpp
#include "String.hpp"
namespace {

class StringRep {
	friend class String;
	StringRep(const char* s);
	~StringRep();
	int count;
	char* rep;
};

}
String::String(){ ... }
...

3、Inheritance (继承),表示is-a

语法:class 子类 :public 父类,其中public也可以改为private,protected

struct _List_node_base//父类
{
	_List_node_base* _M_next;
	_List_node_base* _M_prev;
};

template<typename _Tp>
struct _List_node : public _List_node_base //子类
{
	_Tp _M_data;
};

子类会继承父类的数据、函数,其中最有价值的地方是和虚函数打通

两者关系图如下,base为父类,derived为子类

在这里插入图片描述

从构造函数来看,由内而外,子类的构造函数首先调用父类的默认构造函数,然后才执行自己

从析构函数来看,由外而内,子类的析构函数首先执行自己,然后才调用父类的析构函数,父类的析构函数,必须是 virtual,否则会出现undefined behavior

Inheritance(继承)+Composition(复合)关系下的构造和析构

在这里插入图片描述

  • 构造由内而外: Derived(子类)的构造函数首先调用Base(父类)的default构造函数,然后调用Componentdefault构造函数,然后才执行自己。(红色部分编译器自动加的!)
  • 析构由外而内: Derived(子类)的析构函数首先执行自己,然后调用Component的析构函数,然后才调用Base(父类)的析构函数

十二、虚函数与多态

1、虚函数

non-virtual 函数:你不希望子类重新定义(override, 覆写) 它.

virtual 函数:你希望子类重新定义(override, 覆写) 它,且你对它已有默认定义。

pure virtual 函数:你希望子类 一定要重新定义(override 覆写)它,你对它没有默认定义。

class Shape {
public:
	virtual void draw( ) const = 0;//pure virtual,形式就长这样
	virtual void error(const std::string& msg);//impure virtual
	int objectID( ) const;//non-virtual
	...
};

class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

在重写虚函数时,所有的关键字、函数名、参数都要一样

下列例子包含了继承+委托

父类:

class Component
{
	int value;
	public:
		Component(int val) { value = val; }
		virtual void add( Component* ) { } // 虚函数
};

子类1:

class Primitive: public Component
{
	public:
		Primitive(int val): Component(val) {}
};

子类2:

class Composite: public Component
{
	vector <Component*> c;
	public:
		Composite(int val): Component(val) { }
		void add(Component* elem) { // 重写虚函数
			c.push_back(elem);
		}
	…
};

2、多态

在这里插入图片描述

Image父类

#include <iostream.h>
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;
};
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();
}

LandSatImage子类

class LandSatImage: public Image
{
public:
	imageType returnType() {
		return LSAT;
	}
	void draw() {
		cout << "LandSatImage::draw " << _id << 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;

SpotImage子类

class SpotImage: public Image
{
public:
	imageType returnType() {
		return SPOT;
	}
	void draw() {
		cout << "SpotImage::draw " << _id << endl;
	}
	mage *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;

main

// 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];
}

十三、转换函数(conversion function)

1、转出去

把class A 转成另一种class,语法:operator+空格+另一种类型,以分数这类为例:

class Fraction {
public:
	Fraction(int num, int den = 1) :m_numerator(num), m_denominator(den) {}
	operator double() const { // 转换函数,没有参数,不用写返回类型,通常加上const,表示不改变
		return ((double)m_numerator / m_denominator);
	}
private:
	int m_numerator;  //分子
	int m_denominator;//分母
};
Fraction f(3,5);
double d = 4 + f;//调用operator double()将f转为0.6

编译器先去查找是否有支持的函数能让这行代码通过,首先找全局函数是否有operator+(double,Fraction),重载了+号,发现没有,则看能否将f转换为double。找到了operator double() const,于是f变成了0.6。

2、转过来

把别的class转成class A,使用non-explicit-one-argument构造函数,

class Fraction
{
public:
	Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}//该构造函数是,只有一个实参的构造函数,且没有explicit关键字
    
    Fraction operator+(const Fraction& f){
        return Fraction(...);
    }
private:
	int m_numerator;  
	int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;//调用non-explicit 构造器将4转为Franction(4,1),然后调用operator+

在Fraction类中我们重载了+运算符,可以使两个Fraction对象进行相加,但是对于一个整数与一个Fraction对象进行相加,于是编译器去看看能不能将4转换为Fraction,如果可以转换,则符合了我们的+重载,于是调用构造函数Fraction(int num,int den = 1),将4转换为Fraction,进行加法

3、转换冲突

将上面两个例子中的两个成员函数整合。

class Fraction
{
public:
	Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
    
    operator double()const {
		return ((double)m_numerator / m_denominator);
	}
    
    Fraction operator+(const Fraction& f){
        return Fraction(...);
    }
private:
	int m_numerator;  
	int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;// error 有歧义

解决方法

给构造函数添加explict关键字,explicit多数用在构造函数处,少数还有在模板处

class Fraction
{
public:
	explicit Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
    
    operator double()const {
		return ((double)m_numerator / m_denominator);
	}
    
    Fraction operator+(const Fraction& f){
        return Fraction(...);
    }
private:
	int m_numerator;  
	int m_denominator;
};

给构造函数添加explict关键字,此时别的类无法转换为Fraction对象,转过来的路走不通,只能转出去

Fraction f(3,5);
double d = 4 + f;

f被转成double进行计算,结果为4.6

注:不能写成double d = f + 4;,因为explicit的关系,4不能转换成Fraction对象,会报错

十四、类指针类(pointer-like classes)

设计一个类,让它的行为像指针

1、智能指针

智能指针就是一种经典的“像一个指针的类”,这个class创建的对象像指针,要比普通指针多做一些事情

智能指针中一定有一个一般的指针(下图中,大的圆圈为智能指针,中间的点为一般指针)

它一定有 * 、—>这两个操作符的重载,且实现手法都是固定的

image-20220722175624295

注:"->"这个符号很特别

  • 例如说,上图右侧中的* sp中的,* 号,在使用后就会消失。
  • 但是sp->method(),我们可以看到,调用sp->在右侧的类中,返回px,再往下看px->method(),会发现,这里其实少了一个->,这里就体现出这个符号的特殊性了,得到的东西会继续用箭头符号作用上去。

2、迭代器

迭代器是另一种pointer-like classes的指针

迭代器中一定有一个一般的指针

除了有 * 、—>这两个操作符的重载,还有++、--等等其他操作符的重载。且 * 、—>重载的实现手法与智能指针不一样。

img

上图为链表的示例,其中迭代器为图中的泡泡,

十五、仿函数(function-like classes)

设计一个类,让它的行为像函数

一个函数需要()操作符,即function call operator,如果一个东西能接受()操作符,那么我们就把这个东西叫做函数,或像函数的东西。

image-20220731175954580

以上为标准库的例子,以第二个为例,使用select1st方式为:select1st<Pair> () (),第一个()为创建临时对象,第二个()才是调用操作符。

标准库中,有很多仿函数,它其实是一个很小的类,这些仿函数都继承了一些“奇怪”的父类,这些“奇怪”父类的大小理论上为0,且没有函数,只有一些typedef定义。(详见标准库课程)

十六、成员模板(member template)

也就是模板的嵌套,模板中有模板,如下图黄色部分

image-20220731195410159

黄色这一块是当前模板的一个成员,同时它自己也是个模板。所以它就叫做成员模板

T1,T2可以变化,U1,U2也可以变化。

在STL标准库中会大量出现成员模板,先来一个小示例:

这里T1为鱼类,U1为鲫鱼,T2为鸟类,U2为麻雀

鲫鱼类继承自鱼类,麻雀类继承自鸟类

使用鲫鱼和麻雀构成的pair,然后拷贝到到鱼类和鸟类构成的pair,这样是可以的。反之则不行。允许或不允许限制的条件为: 下方代码中的构造函数。(父类指针可以指向子类对象)

下面为另一个例子

image-20220925210959276

父类指针可以指向子类对象,可以实现up-cast为向上构造

十七、模板特化(specialization)

泛化的反面就是特化,特化就是根据特定的类型进行特殊处理

// 泛化
#include <iostream>
using namespace  std;

// 泛化
template<class  Key>
struct hash1{};


// 特化
template<>
struct hash1<char>{
    size_t operator()(char x)const {
        return x;
    }
};

template<>
struct hash1<long>{
    size_t operator()(long x)const {
        return x;
    }
};

int main(void){
    // 调用
    hash1<long>()(1000);// 构造一个hash的临时对象,传递参数1000,找到上面的特化long

    return 0;
}

以上为全特化,与之相对应的是偏特化(局部特化)

偏特化一个是从个数上的偏,另一个是从范围上的偏

首先,个数的偏

即将模板中的某个/些参数提前进行”绑定“,从左边开始绑定,不能跳

img

范围上的偏

什么类型都可以传的模板,偏特化为只有指针类型能传的模板。(当然,指针可以指向任意类型,但指针也是一种类型,所以范围确实变小了),从接收任意范围T,到接收指针T*,算是对范围上的特化

img

十八、模块模板参数(tempalte template parameter)

模板的参数又是一个模板,语法就是下面黄色的部分,如:

image-20221223192522559

如上图所示,传递任意的容器与元素类型进行组合,其中第一个打岔的部分,光看语法上并没有问题,但是,实际上在我们定义容器list的时候有多个默认参数,这样做是无法通过编译的,如果只有一个默认参数,那么是可以的。

下图中,黄色部分其实不是模板模板参数

image-20221223205927384

调用中我们使用第二种方法,指明第二模板参数,其实这个list< int >就已经不是模板了,已经指明了,即使它是用模板设计出来的东西。
但是已经绑定,写死,list中的元素类型为int;注意与上一张图对比,所以temp<class T,class Sequence = deque< T >>第二个参数,不是模板模板参数。

十九、可变参数模板(variadic templates-since)

语法就是typename...Types&...args...,表示参数数量可变,如果你想确定这“一包”参数具体有多少个,可以用语法:sizeof...(args)

#include <iostream> 
#include <bitset>
using namespace std;

void print(){}


template<typename T,typename... Types>
void print(const T& firstArg,const Types&...args) {//参数分为两组,第一组只有一个参数,第二组参数个数不知道
   cout<<firstArg<<endl;
   print(args...);// 调用,将这一包拆开 
   // 注意: 到最后变成0个的时候,将会调用上面的print() 
   cout<<sizeof...(args)<<endl;// 获得这一包中有几个元素 
}
int main(void){
   print(7.5,"hello",bitset<16>(377),42);//调用
   return 0;
}

二十、自动类型推导(auto)

auto其实就是一个语法糖,面对复杂的返回类型可以用auto自动推导出来。

示例:

list<string>c;
...
list<string>::iterator ite;
ite = find(c.begin(),c.end(),target);

其中list<string>::iterator ite;,太过复杂,可以使用auto,改为:

list<string>c;
...
auto ite = find(c.begin(),c.end(),target);

//以下为错误例子
auto ite;// 编译器不能也无法知道这个ite是什么,无法进行推导
ite = find(c.begin(),c.end(),target);

二十一、更简洁的for(ranged-base for)

范围for循环,也是C++11的一个语法糖。它有如下特点:

  • 它有两个参数,一个是自己创建的变量,另一个是一个容器
  • 范围for循环可以将一个容器(第二个参数)里的元素依次传第一个参数,并在该循环体中依次对每一个元素做操作。
  • 如果你不想影响容器中的参数,请pass by value,否则请pass by reference

示例:

vector<double>vec;
//...

// pass by value 传值,里面的操作不会改名vec里原来的东西
for(auto elem:vec){
    cout<<elem<<endl;
}
// pass by reference 传引用——改变vec原来的东西
for(auto& elem:vec){
    elem *=3;
}

二十二、引用(reference)

编译器其实把reference视作一种pointer。引用有如下特点:

  • 引用是被引用对象的一个别名
  • 引用一定要有初始化
  • object和其reference的大小相同,地址也相同(全都是假象
  • reference通常不用于声明变量,而**用于参数类型(parameters type)和返回类型(return type)**的描述。
  • 在函数调用的时候,pass by value和pass by reference传参形式一样,而且reference比较快,所以推荐一般pass by reference。(请回顾何时不可以pass by reference)
	int x = 0;
	int* p = &x;// p为指针、类型为point to int,初值为x的地址,指向x,写法上推荐*靠近int,之后可以修改指向
	int& r = x;// r类型为reference to int,代表x,从此之后,r不能代表其他,x现在为0,所以r也为0,现在r不是指针,x现在为int,所以r也为int
	
	int x2 = 5;
	
	r = x2;// r不能重新代表其他,现在x r都为5,相当于将值5赋给x 
	
	int& r2 = r; //r2 = 5,r2代表r,也相当于代表x

注意:

  • sizeof(r)== sizeof(x)相等
  • &x == &r相等

因为这是编译器制造出的假象,大小相同,地址也相同,举例验证如下:

typedef struct Stag {int a, b, c, d;} S;
int main(){
    double x = 0;
    double* p = &x; //p指向x,p的值是x的地址
    double& r = x;  //r代表x,现在r、x的值都是0
    
    cout << sizeof(x) << endl; //8
    cout << sizeof(p) << endl; //4,指针在32位电脑里为4个字节
    cout << sizeof(r) << endl; //8
    cout << p << endl;  //0065FDFC
    cout << *p << endl; //0
    cout << x << endl;  //0
    cout << r << endl;  //0
    cout << &x << endl; //0065FDFC
    cout << &r << endl; //0065FDFC
    
    S s;
    S& rs = s;
    cout << sizeof(s) << endl;  //16
    cout << sizeof(rs) << endl; //16
    cout << &s << endl;  //0065FDE8
    cout << &rs << endl; //0065FDE8
}

referece 就是一种漂亮的pointer,多用于参数传递——传引用

image-20221223233254829

注意以下两者不能共存:

double imag(const double& im){...}
double imag(const double im){...}

因为两者的same signature相同,即 imag(const double& im) = imag(const double im),如果改为imag(const double& im) const,那么两者same signature不同,即imag(const double& im) const ≠ imag(const double im),可以共存

二十三、虚指针(vptr)和虚表(vtbl)

img

现有三个类ABC,三者之间为继承的关系,继承父类函数,继承的是调用权

请注意:A B C三个类的非虚函数,即func1、func2,他们虽然重名,但其实彼此之间毫无关系,这一点要注意,千万别以为B的非虚函数是继承A的非虚函数。

B类的内存大小= 继承A类的数据 + B本身的数据,C类同理。(关系图最右边)

A有两个虚函数vfunc1vfunc2以及两个非虚函数func1func2

B类继承A类的vfunc2,同时覆写A类的vfunc1,此时B有两个虚函数(vfunc1和vfunc2)

C类继承了B类的vfunc2vfunc2其实是A的),同时覆写了vfunc1,也有两个虚函数。

所以A B C这三个类一共有八个函数,四个非虚函数,四个虚函数。(关系图中间偏右)

只要一个类拥有虚函数,则就会有一个虚指针vptr,该vptr指向一个虚表vtbl虚表vtbl中放的都是函数指针,指向虚函数所在的位置。(可以观察到,关系图中虚表中的函数指针都指向相应的虚函数的位置)这其实就是动态绑定的关键。

如果创建一个指向C类的指针p(如C* p= new C),如果让该指针p调用虚函数C::vfunc1(),则编译器就知道这是动态绑定,故这时候就会通过p找到vptr,再找到vtbl,最终通过vtbl的函数指针找到相应的虚函数。该步骤如果要解析成C语言的话就是图片中灰框所示,其中n指的是要调用的虚函数在虚表中的第几个。n在写虚函数代码的时候编译器看该虚函数第几个写的则n就是几。

与虚函数相关的就是多态

例如一个容器,其元素是指针类型,它的功能是要画出不同的形状,所以要用虚函数,子类要进行覆写实现自己的形状绘制。调用虚函数的过程为动态绑定——即多态父类指针可以接收具体的子类对象,即根据具体是哪个子类,调用该虚函数具体的形式

img

总结一下,C++编译器看到一个函数调用,它有两个考量:

  • 是静态绑定吗?(Call ×××)
  • 还是动态绑定。

要想动态绑定要满足三个条件:

  • 第一:必须是通过指针来调用
  • 第二:该指针是向上转型(up-cast)的
  • 第三:调用的是虚函数

到此为止,多态、虚函数、动态绑定是指的一回事了

二十四、this

类的成员函数中,默认会有一个this指针传递进来。由编译器自己处理

当你通过一个对象调用一个函数,该对象的地址就是this指针

img

二十五、Dynamic Binding动态绑定

现在考虑一下,为什么动态绑定解析成C语言形式会是:(*(p->vptr)[n])(p)(* p->vptr[n])(p),第二个p其实就是this指针。

从汇编角度看一下:

下图中 a是一个对象,它调用函数是一个静态绑定,可以看到汇编呈现的就是:Call xxx一个地址

img

下图中pa满足动态绑定的三个条件,所以它是一个动态绑定,而在汇编语言中,汇编所呈现出来的那部分命令就等价于C语言中的(*(p->vptr)[n])(p)

img

二十六、const

const用于修饰成员函数——即放到成员函数参数列表后,表明该成员函数不打算修改成员变量的值

  • const也是函数签名的一部份,即可构成函数重载。
  • 常量对象不可以调用非常量成员函数
  • 当成员函数的constnon-const版本同时存在const对象只能调用const版本的成员函数,non-const对象只能调用non-const版本的成员函数。
  • non-const成员函数可调用const成员函数,反之则不行

二十七、重载Operator new,operator delete

全局重载

在前面加上::,表示全局重载,即**::operator new, ::operator delete, :: operator new[], ::operator delete[]**

img

重载member operator new/delete

如下所示,重载一个类中的成员,这里注意一下delete重载的第二个参数时可选的,可以不写。

img

重载member operator new[] / delete[]

img

具体的例子,如下:

img

重载new()

要求:

  • 示例:Foo* pf = new(300,'c') Foo;
  • 可以重载多个class member operator new()版本,但每一个版本的参数列表必须独一无二
  • 且参数列表的第一个参数必须为size_t,其余参数为使用时()中指定的参数。出现在new(…)小括号内的便是所谓的placement arguments,示例中的300,‘c’
  • 所以上述的使用形式小括号内虽然看到有两个参(300,‘c’),其实有三个。

image-20221225190302180

重载delete()

要求:

  • 可以(也可以不)重载多个class member operator delete()版本,但绝不会被delete调用(这个delete是指可以被分解为两步的那个delete)
  • 唯一被调用的时机:只有当new所调用的构造函数(new被分解的第一步)抛出异常,才会调用与new对应的那个重载operator delete(),主要用来归还未能完全创建成功的对象所占用的内存

img

placement new的应用:Basic_String使用new(extra)扩充申请量

标准库中对placement new的一个应用,用于无声无息的扩充申请一部份内存。

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值