Skip to content

C++ Part1 面向对象编程合集

6107字约20分钟

C++

2023-08-16

1 头文件与类的声明

1.1 c vs cpp关于数据和函数

c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的

cpp中,将数据data和函数都包含在一起(class),创建出一个对象,即为面向对象;数据和函数(类的方法)都是局部的,不是全局的

class的两个经典分类:

  • 无指针成员的类(complex)——复数
  • 有指针成员的类(string)——字符串

1.2 头文件与类

1.2.1 头文件

引用自己写的头文件,用双引号

头文件的标准写法:

complex.h:

#ifndef _COMPLEX_  // 如果没有被定义过就定义 (防卫式声明)
#define _COMPLEX_

#endif
  • 首先是防卫式声明,如果没定义这个名词,那么就定义一下。ifndef+define。(这样如果程序是第一次引用它,则定义,后续则不需要重复定义,不需要重复进入下面的过程)
  • 1要写的类的声明,2是要写类的具体定义,写1 2的时候发现有一些东西需要提前声明,写在0

1.2.2 class的声明

在C++中 struct和class唯一的区别就在于默认的访问权限不同

  • struct 默认权限为公共
  • class 默认权限为私有
class complex  //class head
{              //class body  /*有些函数直接在此定义,另一些在 body 之外定义*/
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&); 
};
{
 complex c1(2,1);
 complex c2;
 ...
}

1.2.3 模板初识

{
    complex<double> c1(2.5, 1.5);
    complex<int> c2(2, 6);
    ...
}
  • 因为实部和虚部的类型不确定,可能是 double float int,定义一个模板类型叫做 T
  • T作为一个类型参数来传入,在调用的时候就可以指定类型了
  • 通过在定义类的前面加入一行代码 template<typename T> 来实现

2 构造函数

2.1 inline 函数

定义类的时候,可以直接在body中定义函数(inline函数,在body中定义完成),也可以只是在body中声明函数

  • inline内联函数:如果定义的函数是内联函数,那么会运行比较快,尽可能定义为内联函数
  • 在body外,通过inline关键字来指定该函数为inline函数

注意的是,上面所有的inline函数,都只是我们指定的,希望它为inline,具体是不是,要看编译器来决定

2.2 访问级别

  • 数据应该被定义为private

  • 函数要被外界使用,定义为public;若只是内部处理,定义为private

2.3 ctor 构造函数

2.3.1 ctor 的写法

方式一:(推荐)

complex(T r = 0, T i = 0) //函数名称与class的名称一致
    : re(r), im(i)        //中间这一行就是初始化
{ }

方式二:(不推荐)

complex(double r = 0, double i = 0)  
{
    re = r; im = i;       //用赋值来进行初始化
}

通过构造函数来创建对象。会自动调用构造函数进行创建。

  • 构造函数名称需要与类的名称一样
  • 函数的参数可以有默认参数
  • 构造函数没有返回类型

2.3.2 ctor/函数 重载

构造函数可以有很多个,可以重载;但是上面的1 2两个构造函数冲突了

complex c2();   // "()" 可以不要,一样的

上面的调用方式对两个构造函数都适用,冲突


double real () const { return re; }
void real (double r) {  re = r;  }  //不能有const
  • 同名的函数可以有多个,编译器会编成不同的名称,实际调用哪个会根据哪个适用

2.3.3 ctor 放在 private 区

  • 通常构造函数不要放在private中,这样外界没法调用,也就无法创建对象
  • 在设计模式 Singleton(单体)中,将构造函数放在了private中;这个class只有一份,外界想要调用的时候,只能使用定义的 getInstance() 函数来取得这一份;外界无法创建新的对象

2.4 const 常量成员函数

对于不会改变数据内容的函数,一定要加上const

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

对于上面调用方式,我们创建一个常量复数然后调用函数输出实部虚部,如果上面real和imag函数定义的时候,没有加const,那么这里函数默认的意思是可能会改变数据,与我们的常量复数就矛盾了,编译器会报错;因此,对于不会改变数据内容的函数,一定一定要加const

3 参数传递与返回值——引用

3.1 参数传递

  • 值传递 pass by value,传递value是把整个参数全传过去,尽量不要直接value传递double r

  • 引用传递 pass by reference,传引用相当于传指针,快,形式也漂亮 例 complex&

  • 如果只是为了提升速度,不向改变数据,那么传const引用;这样传进去的东西,不能被修改

    const complex&

3.2 返回值传递

返回值的传递,尽量返回引用

在函数中创建的变量 (local 变量),要返回——这种情况是不能返回引用的;因为函数结束后函数中创建的变量就消失了,无法引用


传递者无需知道接受者是以reference形式接受——所以用reference形式很便捷

4 友元 friend

4.1 友元

友元:friend,修饰在函数定义之前表示这个函数可以直接拿该类对象的private数据

inline complex&
__doapl(complex* ths, const complex& r)
{
    ths->re += r.re;  //直接拿private的数据,不需要函数
    ths->im += r.im;
    return *ths;
}
  • 如上面所示,声明为friend之后,函数可以直接取到re和im,如果不被声明为friend,只能通过调用real和imag函数来得到,效率较低

4.2 相同 class 的 object 互为 friends

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

相同class的不同对象互为友元,即可以直接取另一個 object 的 private data

5 操作符重载与临时对象

5.1 操作符重载

在c++里我们可以定义加法等操作符,比如我们可以定义两个石头的加法

5.1.1 成员函数实现 / this

成员函数: complex :: function .... 前面带有class的名称(在class里先声明了的)

inline complex&
complex::operator += (const complex& r) {
    return __doapl(this, r);   //do assignment plus
}

所有的成员函数都带有一个隐藏的参数this是一个指针),this指向调用这个函数的调用者

  • 定义函数的时候,在参数中不能写出来this,直接用即可

  • 函数里可写可不写,但当传入参数成员变量名相同时要写

    public:
    	double real () const { return this->re; }  //这里的this->可省略

c3 += c2 += c1;    // c2 加了 c1 后如果返回 void 就无法进行 c3 的操作了

将操作符写为void函数也可以,但为了可以兼容c3+=c2+=c1的形式,写成返回引用更好。

5.1.2 非成员函数实现

非成员函数没有this

应对三种使用方法,写出三种方式

  • 非成员函数是global函数——为了后面两种使用方法

  • 这些函数不能返回引用,必须值传递

    在函数中创建的新变量 (local 变量),要返回

5.1.3 output函数 << 的重载

cout不认识新定义的这种复数,因此也需要对<<进行操作符重载

只能全局函数,不能成员函数——导致使用时方向相反

#include <iostream.h>
ostream&
operator<<(ostream& os, const complex& x)
{
    return os << '(' << real(x) << ',' << imag(x) << ')';  //自定义输出
}
  • ostream&cout 的 classname

参数传递:os 在函数中会变化,所以不能加 const

返回值传递:为了避免 cout << c1 << conj(c1); 连续输出,不用 void

cout << c1 返回值需要与 cout 类型一致

5.2 临时对象

classname () 创建一个classname类型的临时对象——不需要名称,生命只有一行

6 带指针的类:三大函数

  • 析构函数:~String();

  • 拷贝构造函数 copy ctor : String (const String& str); —— string s3(s1)

  • 拷贝赋值函数 copy op= : String& operator=(const String& str); —— s3=s2

    编译器默认的拷贝构造赋值(一个bit一个bit的复制),编译器默认的只是拷贝了指针(浅拷贝),而不是指针指向的数据

    alias(别名)和 memory leak(内存泄漏)都是十分危险的

    因此,如果类中有指针,一定自己写这两个函数

6.1 ctor 和 dtor (构造和析构函数)

6.1.1 ctor 构造函数

这里的 new 是申请的字符串的空间

inline
String::String(const char* cstr = 0)
{
    if (cstr) {       // 指定了初值—— String s2("hello");
        m_data = new char[strlen(cstr) + 1];  // 字符串长度 + /0
        strcpy(m_data, cstr);
    }
    else {            // 未指定初值—— String s1();
        m_data = new char[1];
        *m_data = '\0';
    }
}

这里的 new 是申请的指针的空间String()里面还有一个 new

String* p = new String("hello");  
delete p;

6.1.2 dtor 析构函数

inline  
String::~String()
{
    delete[] m_data;
}

每个 new 都对应一个 delete —— 一定要释放

类对象死亡的时候(离开作用域),析构函数会被自动调用

例:这里结束会调用三次 dtor

{
    String s1(),
    String s2("hello");
    String* p = new String("hello");
    delete p;
}

6.2 copy ctor 拷贝构造函数

inline
String::String(const String& str)
{
    m_data = new char[strlen(str.m_data) + 1]; // “str.m_data” 兄弟之间互为友元 
    strcpy(m_data, str.m_data); // 深拷贝
}
String s1("hello ");
String s2(s1);

6.3 copy op= 拷贝赋值函数

  1. 先杀死调用者

  2. 重新申请指定大小的空间

  3. 复制字符串内容到调用者

inline
String& String::operator=(const String & str)
{
    if (this == &str)  // 检测自我赋值 self assignment
        return *this;

    delete[] m_data;                               // 第一步
    m_data = new char[strlen(str.m_data) + 1];     // 第二步
    strcpy(m_data, str.m_data);                    // 第三步
    return *this;
}

一定要在开始就检测自我赋值,因为a=a时第一步 delete 了后,会使第三步出现问题

7 堆,栈,内存管理

7.1 堆和栈

Stack ,是存在于某作用域 (scope) 的一块内存空间。

例如当你调用函数,函数本身即会形成一个 stack 用来放置它所接收的参数,以及返回地址;在函数本体 (function body) 内声明的任何变量其所使用的内存块都取自上述 stack

Heap ,或称为 system heap ,是指由操作系统提供的一块 global 内存空间,程序可动态分配 (dynamic allocated) 从中获得若干区块 (blocks)

可以用 new 来动态取得

在 stack 中的是自动生成的空间,作用域结束空间会自动释放

在 heap 中的是自己申请的空间,需要自己释放

{
   complex c1(1,2);              
   /*c1空间来自stack*/
   complex* p = new complex(3);  
   /*complex(3) 是个临时对象
   其所用的空间是以new从heap动态分配而得,并由p指向*/
}

7.2 object 生命期

  • stack objects 的生命期

    c1 便是所谓 stack object,其生命在作用域 (scope) 结束之际结束这种作用域内的 object,又称为 auto object,因为它会被“自动”清理(结束自动调用析构函数)

    {
    	complex c1(1,2);
    }
  • static local objects 的生命期

    若在前面加上 static 后,其会存在到整个程序结束

    {
        static complex c2(1,2);
    }
  • global objects 的生命期

    写在任何作用域之外的对象,其生命在整个程序结束之后才结束,你也可以把它视为一种 static object,其作用域是整个程序

    ...
    complex c3(1,2);
    
    int main()
    {
        ...
    }
  • heap objects 的生命期

    p 所指的便是 heap object,其生命在它被 delete 之际结束

    {
        complex* p = new complex;
        ...
        delete p;
    }

7.3 new 和delete

7.3.1 new

new:先分配 memory , 再调用 ctor

image-20230731092332395
  1. 分配内存:先用一个特殊函数,按 class 的定义分配了两个 double 的大小
  2. 转型(忽视)
  3. 调用构造函数,赋值(1,2)

7.3.2 delete

delete:先调用 dtor, 再释放 memory

image-20230731092947259
  1. 调用析构函数——释放的是 m_date 指向的字符串 Hello 的空间(即构造函数中 new 申请的空间)
  2. 释放内存:用一个特殊函数释放了 ps 指向的空间(即String* ps = new String("Hello");new 申请的空间)

7.4 内存动态分配

7.4.1 在VC下内存动态分配

在VC下(不同编译器的内存动态分配可能不同)

image-20230731095853726
  • 调试模式:

    (4*3) 是3个指针的大小

    (32+4) 是调试模式所需空间(橘色部分)

    (4*2) 是上下两个 cookie ——表示内存块的开始与结束

    4 是数组才有的长度记录

    由于分配内存块需要是16的倍数,所以需要 pad 来填充到64

  • 执行模式:

    去掉调试模式的空间即可

因为内存块是16的倍数,因此最后四位bit一定都是0,cookie 就借用最后的一位1表示占用内存,0表示释放内存

如上图41h1即表示占用内存

7.4.2 array new/delete

image-20230731101729210

array new 一定要搭配 array delete

new后有[ ]—> delete后加[ ]

普通的delete只调用一次析构函数——剩下两个指针的指向的空间没有调用析构函数,内存泄漏

这种情况发生在有指针的类,但最好都这样写

8 静态 模板 namespace

8.1 static

对于非静态的函数和数据:

非静态的成员函数通过this指针来处理不同的数据(一份函数--->多个对象)

image-20230731154548833

对于静态的函数和数据:

静态函数没有this,不能处理一般的数据,只能处理静态的数据

例1:

class Account
{
public:
	static double m_rate;  //静态变量的声明
	static void set_rate(const double& x) { m_rate = x; } //静态函数
};
double Account::m_rate = 0; //静态变量的定义 一定要有

int main()
{
    //调用静态函数法1——by class name
	Account::set_rate(5.0);
    //调用静态函数法2——by object
	Account a;
	a.set_rate(7.0); //静态函数与a无关/无this
}

例2:设计模式 Singleton(单体)

image-20230731163117925
  • 构造函数放在private中,外界无法调用
  • 设计了getInstance静态函数,来生成并返回唯一的一份

8.2 template

8.2.1 class template 类模板

  • T来代替某种类型
  • 使用时classname<type1> xxx,编译器会把T全部替换为type1

8.2.2 function template 函数模板

image-20230731164908165

比较函数——任何类型都可以进行比较;T来代替某种类型

应用时,不需要写某种类型——编译器自己会推导

8.3 namespace

对东西进行一个包装(不一定要一次性全写在一起,可分开包装在一起)

namespace name
{
...    
}
  1. 用法一:using directive

    #include <iostream>
    using namespace std; //直接把包装全打开
    int main()
    {
    	cin << ...;
    	cout << ...;
    	return 0;
    }
  2. 用法二:using declaration

    #include <iostream>
    using std::cout; //只打开一条
    int main()
    {
    	std::cin << ...; //没打开要写全名
    	cout << ...;
    	return 0;
    }
  3. 用法三:都写全名

    #include <iostream>
    int main()
    {
    	std::cin << ; 
    	std::cout << ...;
    	return 0;
    }

9 复合 委托

9.1 Composition 复合

类似于c中结构里有结构——class里有class

image-20230801093748678

deque 是一个已经存在的功能很多的类(两头进出的队列);利用deque的功能来实现queue的多种操作

该例只是复合的一种情况——设计模式 Adapter

9.1.1 复合下的构造和析构

image-20230801095529359
  • 构造是由内而外

    Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己

    注意如果要调用 Component 的其他构造函数需要自己写出来

    Container::Container(…): Component() { … };

  • 析构是由外而内

    Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数

9.2 Delegation 委托

委托就是 Composition by reference;即通过指针把任务委托给另一个类

复合中,内部和外部是一起出现的;而委托是不同步的

这是一个著名的设计模式——pimpl (pointer to implementation) 或者叫 “编译防火墙”

  • 右边怎么变动都不会影响左边

  • reference counting 多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,b和c依然在共享

    image-20230801101907977

10 继承与虚函数

10.1 Inheritance 继承

语法::public base_class_name

public 只是一种继承的方式,还有protectprivate

子类会拥有自己的以及父类的数据

10.1.1 继承下的构造和析构

与复合下的构造和析构相似

image-20230801150053963
  • 构造是由内而外

    Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己

    注意如果要调用 Component 的其他构造函数需要自己写出来

    Derived::Derived(…): Base() { … };

  • 析构是由外而内

    Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数

    Derived::~Derived(…){ … /* ~Base() */ };

    注意:Base class 的 dtor 必需是 virtual

    否则下例会导致结束时只会调用 Base 的 dtor

    int main() {
        Base* ptr = new Derived();
        delete ptr; // 只会调用 Base 类的析构函数
        return 0;
    }

10.2 虚函数

image-20230801152023433
  • pure virtual 函数:

    derived class 一定要重新定义 (override 覆写) 它;其没有定义只有声明

    语法:virtual xxxxxx =0;

  • virtual 函数:

    derived class 可以重新定义 (override, 覆写) 它,且它已有默认定义

    语法:virtual xxxxxx;

  • non-virtual 函数:

    不希望 derived class 重新定义 (override, 覆写) 它

10.3 继承 with virtual

例子:在 Windows 平台下用某个软件打开文件——分为好几步,但基本所有软件大多数操作都是一致的,只有一个操作如读取方式是不一样的

image-20230801154005427
  1. 现有一个框架 Application framework 其写好了所有必要的函数,其中 Serialize() 就是一个 pure virtual 函数
  2. 使用这个框架写自己软件的打开文件,就继承这个框架,其中就需要自己 override 覆写 Serialize() 这个函数
  3. 在执行中,执行 myDoc.OnFileOpen(); 中到 Serialize() 时,是通过 this 来指引到自己写的 Serialize() 中去的

把关键动作延缓到子类再做,这是一个经典的设计模式——Template Method

10.4 缩略图

  • 复合:image-20230802084858622

  • 委托:image-20230802085101744

  • 继承:image-20230802085210589

  • 类中的元素:image-20230802085810812 变量名称 : 变量类型(与代码刚好相反

    • 变量下面加下划线 表示 static

    • 前面加一个 - 表示 private

    • 前面加一个 # 表示 protected

    • 前面加一个 + 表示 public(一般可以省略)

10.5 继承+复合

这种关系下的构造和析构与之前的类似

  • 第一种:

    image-20230801161457590
    • 构造由内到外 先 Base 再 Component

      Derived 的构造函数首先调用 Base 的 default 构造函数,然后调用 Component 的 default 构造函数,然后才执行自己

      Derived::Derived(…): Base(),Component() { … };

    • 析构由外而内 先 Component 再 Base

      Derived 的析构函数首先执行自己,然后调用 Component 的析构函数,然后调用 Base 的析构函数

      Derived::~Derived(…){… /*~Component() ~Base()*/};

  • 第二种:

    image-20230801162202797

    同理构造由内到外,析构由外而内

10.6 继承+委托

10.6.1 例一 Observer

设计模式—— Observer

例如一串数据,可以用饼图来观察,也可以用条形图来观察,这种种的观察方式都是继承于 Observer

image-20230801163932926

通过 vector<Observer> m_views; 来进行委托

当数据改变的时候,Observer 也需要更新,即 notify 函数,来将目前所有的观察者更新

10.6.2 例二 Composite

设计模式—— Composite

例如文件系统,文件夹里可以有文件夹(与自己相同的类),也可以有文件,其中文件就是最基本的 Primitive,而文件夹就是复合物 Composite

image-20230802082524919

要达成目的,就可以再设计一个父类 Component ,文件和文件夹就继承于同一父类;

其中 Composite 要用委托到父类的方式 Component* 设计容器和操作——使其 PrimitiveComposite 都可以适用

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

//复合物 Composite
class Composite 
    : public Component
{
	vector <Component*> c;  
public:
	Composite(int val) : Component(val) {}

	void add(Component* elem)
	{
		c.push_back(elem);
	}

}

//基本类 Primitive
class Primitive
    : public Component
{
public:
	Primitive(int val): Component(val) {}
};

component中add是虚函数(且是空函数),不能是纯虚函数——Primitive 不会 override add函数(最基本的单位,不能 add 了),而 Composite 需要 override add函数

10.6.3 例三 Prototype

设计模式—— Prototype

框架(父类)要创建未来才会出现的子类——要求子类要创建一个自己当作原型 Prototype 让框架(父类)来找到并创建 FindAndClone

补充:当一个子类继承自父类时,它可以被视为是父类的一种类型,因此可以使用父类的指针或引用来引用子类的对象;

这种用父类的指针或引用来处理子类对象的方式称为——**向上转型 ** Upcasting

image-20230802163941216
  1. 父类中,有一个存放原型的数组,有纯虚函数 Image *clone(),还有两个静态函数 Image FindAndClone(imageType); void addPrototype(Image *image){...}

  2. 子类中,创建一个静态的自己 _LAST ,把它放到父类的一个空间中,这样父类就可以找到新创建的子类

    private 的构造函数 LandSatImage() 中是 addPrototype(this); //这里的 this 就是 _LAST 将自己的原型放到了父类中去

  3. 子类中,准备一个 clone()函数,父类通过调用找到的相应类型的 clone 函数来创建子类的副本

    这里的 clone 函数就不能用之前的那个构造函数来创建副本了——其会放到父类中去,所以创建一个新的构造函数 LandSatImage(int) 用传进一个无用参数(随便传个int型数据就好)来进行区分