侯捷老师《C++面向对象高级编程-1》学习笔记

本文详细讲述了C++编程中良好的类设计规范,包括单类设计(无指针成员)、带指针成员的复杂类设计、C++类的进化、内存布局、对象关系(继承、复合、委托)以及模板方法、静态成员等概念,强调面向对象编程的原则和实践技巧。
摘要由CSDN通过智能技术生成
  • 1.学习目标

1. 培养正规大气的编程习惯

2. 以良好方式编写C++ class,主要是单一class的设计(object based)

  • class without pointer member
  • class with pointer member

3. 学习classes之间的关系, 多个classd的设计(object oriented)

  • 继承inheritance
  • 复合composition
  • 委托delegation

2. 关于C++

1. C++演进:C++98--->C++ 03--->C++ 11---> C++ 14--->C++ 17 ---> C++20 --->C++ 23

2. C++ 包含两部分内容: C++语言 + C++标准库

3. C++ class/struct: 封装数据和函数成为对象

4. 基于对象和面向对象

  • 基于对象(object based): 面向单一class的设计
  • 面向对象(object oriented): 面向多个/重class的设计,侧重于class与class之间的关系

6. class的经典分类

  • class with pointer member(s)
  • class without pointer member(s)

3. 无指针成员变量的单个类设计complex

3.1 inline函数

 类内定义的函数自动成为inline函数,类外定义的函数需要添加inline关键字才成为inline函数。但inline函数只是对编译器的建议,最终是否能成为inline函数依赖于编译器。inline机制使得函数调用处直接采用函数定义代码代替,增加编译时间而减少函数调用运行时间,类似于#difine.

3.2 构造函数ctor

1. 构造函数中优先使用初始化列表,而非赋值方式。

//初始值列表方式,推荐
complex(double r = 0, double i = 0):re(r), im(i){} 
//赋值方式   
complex(double r = 0, double i = 0){ re = r; im = i;}

2. C++同名函数可以有多个,称为重载overloading,编译器编译后重载函数的名称不同。对于构造函数,可以有多个。

3. C++构造函数中可以设置默认值,但要注意默认值的作用。如下两个构造函数同时定义会出现编译问题:

//同时定义会存在编译问题
complex(double r = 0, i = 0): re(r), im(i){}
complex(): re(0), im(0){}

//使用
complex c1;
complex c2();

 4. 构造函数ctor放置在private区域时,不能直接访问对象成员变量值。使用此特性,构造单体模式。使用示例A::getInstance().setup();

//使用示例
A::getInstance().setup();

//C++ Singleton模式示例
class A
{

public:
    static A& getInstance();
    setup() {...}

private:
    //构造函数放置在private区
    A();         
    A(const A& rhs);
    ...
};

A& A::getInstance()
{
    //static静态变量放置在此函数内部,只有真正调用getInstance()时才创建
    static A a;
    return a;
}
 

3.3 const关键字

1. 常量成员函数: 成员函数后面带const,表示不改变成员变量值。

class complex
{
public:
    double real() const{return re;}
    double imag() const{return im;}

private:
    double re, im;
};

complex c1(2, 4);
cout << c1.real() << endl;

2. 带const的变量:表示变量值不允许修改。

3. 在不改变成员变量值的成员方法后面加const,是一个良好习惯。否则,使用者调用类成员方法时可能会出错。如下示例:

class complex
{
public:
    //如下成员方法定义时没有加const
    double real() {return re;}
    double imag() {return im;}
    ...

private:
    double re, im;
};

//const变量调用成员方法,编译器报错
const complex c1(2, 4);
cout << c1.real() << endl;

3.4 参数传递pass by value & pass by reference(to const)

1. C++中参数传递尽量传引用,传引用是比传指针更为优雅的方式,

2. 传引用时如果不需要修改参数,前面可以加const。

3.5 返回值传递return by value & return by reference(to const)

1. 在允许的情况下尽量采用返回引用的方式返回参数。

2. 局部变量或临时对象不能采用return by reference的方式返回.

3.6 friend

1. 使用friend函数可以自由获得类私有成员变量值。

2. 同一个类的各个对象互为friends,一个对象可以直接获得另一个对象的成员变量值。

class complex
{
public:
    int func(const complex& c)
    {
        return c.re + c.im;
    }

private:
    double re, im;
};

{
    complex c1(2, 1);
    complex c2(1, 0);
    c2.func(c1);      //c1 和 c2互为friends
}

3.7 this指针

1.C++类内成员函数内部可以使用this指针,因编译器编译成员函数时中默认会添加this指针的参数。不同编译器放置this指针的位置不确定,this可能是第一个参数,也可能是最后一个参数。

class complex
{
public:
    complex& operator+=(const complex& r);
...
};

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

inline complex&
operator += (const complex& r) //operator += (this, const complex& r), this指针
{
    return __doapls(this, r);
}

3.8 操作符重载

1. 作为成员函数的操作符重载函数,即类内操作符重载,作用于对象上,如3.7中例子。此时在操作符重载函数内部可以使用this指针指代当前对象。

2. 非成员函数的操作符重载函数,不包含this指针,此时可以作用于多个对象。

//c1 += c2;
complex operator += (const complex& x, const complex& y)
{
    return complex(x.real()+y.real(), x.imag()+y.imag());
}

3.9 临时对象

1. 一种形式的临时对象:typename().

{
    complex c1(2, 1);
    complex c2;
    complex();       //临时对象
    complex(4, 5);  //临时对象
    
}

inline
complex operator += (const complex& x, double y)
{
    return complex(real(x)+y, imag(x));     //返回临时对象的值

}

4. 带指针成员变量的单个类设计String

//String.h
#ifndef MYSTRING_H
#define MYSTRING_H

#include <cstring>
#include <iostream>

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

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

ostream& operator<<(ostream& os, const String& str)
{
    os << str.get_c_str();
    return os;
}

#endif

4.1String类构造函数和析构函数

String类含有一个指针类型的成员变量m_data,该指针指向C风格字符串实际保存处。

采用这种指针的方法,可以存储不同长度的字符串。

如下代码,内存中结构如图所示:

  • String *p = new String("hello"); 在栈上申请分配一个String类对象大小的空间,并调用String类的构造函数初始化该空间。其中指针p指向这个空间,该空间内仅存储一个指针变量m_data。
  • 调用String类构造函数时,会申请分配堆空间来存储"hello", 并将m_data指向这块堆存储空间。
  • String类对象不再使用死亡时,调用析构函数释放存储"hello"的堆空间, 即成员变量m_data指向的空间。局部变量p占用的栈空间会自动释放。

4.2 三大函数

  • 拷贝构造函数
  • 拷贝赋值函数
  • 析构函数dtor

4.2.1 含有指针类型成员变量的类必须要有拷贝构造函数和拷贝赋值函数

copy ctor和copy op=函数确保对象间拷贝是深拷贝,每个对象的指针成员变量都有自己的一份存储空间来存储数据。

如下图,使用默认ctor或op=函数,仅仅是浅拷贝,将会使两个对象的指针成员变量指向同一处存储空间,另一个对象的造成内存泄露。

4.2.2 拷贝赋值函数中必须要判断是否是自我赋值(self assignment)

如下图,当自我赋值拷贝时,*this和rhs对象的m_data指针都指向同一块内存。 赋值拷贝时会首先释放*this和rhs对象指针所指内存, 再访问rhs为*this申请分配内存时出现不确定的行为。

4.3 C++内存布局与对象生命周期

内存布局:

  • 栈区stack:存在于某作用域内(如函数)的一块内存空间,存储函数运行时使用的非静态局部变量、函数参数、函数返回值、返回地址等。函数执行完成后,这些变量都将从stack上获自动释放。
  • 堆heap:操作系统提供的一块存储空间,程序可以动态申请。一般需要程序员主动释放。
  • 数据段:包含静态存储区 + 全局存储区
  • 代码段
  • class Complex{...};
    
    //global object, 作用域为整个程序,生命周期直到程序运行结束
    Complex c3(1,2);
    
    {
        //c1所占空间来自stack,是stack object,在该作用域后c1被自动清理释放
        Complex c1(1, 2);
    
        //Complex(3)是个临时对象,其所占空间是由new从heap上动态申请而来,并由c2指向, 该临时对象需要使用delete释放
        Complex *c2 = new Complex(3);
        
        //static object, 其生命在作用域外仍然存在,直到程序运行结束
        static Complex c4(3, 4);
    
        delete c2;   //释放c2所指的heap空间,c2的生命周期到此结束;否则会有内存泄露 
    }

    4.4 关于new和delete操作符

4.4.1 new操作符原理

new操作可以分为三步:

  1. 先分配内存,通过operator new来申请一块对象大小的内存(内部调用malloc)。
  2. 转换内存类型
  3. 执行构造函数

4.4.2 delete操作符原理

delete操作主要有2步:

  1. 执行对象的析构函数dtor
  2. 释放对象所占内存,通过operator delete实现(内部调用free)。

4.4.3 new/delete & new[]/delete[]必须配对使用

1. new和new[]时内存布局

使用new和new[]申请内存,最后都调用malloc()来申请内存。malloc()申请内存的结构如下图所示,包括内存块总大小(红色存储单元,包含两个头尾存储块空间,最后一位标识该块内存是否已经分配 0-未分配/1-已分配)、对象个数、每个对象所占空间。

使用new分配内存示例(32位机器vc环境):

使用new[]分配内存示例(32位机器vc环境):

2. new[]与delete[]搭配使用

如果使用了new String[3]申请了3个String对象的内存空间,而只使用delete p来释放空间,可以看到只会调用一次String的析构函数释放一个String对象中的m_data所申请空间,其他2个String对象中m_data所指向内存不会释放。

即对于含有pointer member的类,如果不正确使用delete[],泄露的是类对象指针成员所指向的动态内存,而不是类对象本身所占用内存

5. 面向对象的类设计

5.1 类和类之间关系

  • 继承inheritance:表示一个类继承自另一个类,两个类是is-a的关系。
  • 复合composition: 表示一个类中包含另一个类的对象,两个类是has-a的关系。
  • 委托delegation:表示一个类中包含有另一个类的对象指针,即composition by reference。

5.2 复合composition

表示一个类中含有另一个类的对象。用实心菱形箭头指向被包含者表示两者之间的关系。

复合Composition关系下的构造和析构:

构造函数由内到外执行:先执行内部Component的default构造函数,再执行自己的构造操作,代码类似于Container::Container(...):Component(){...};

析构函数执行时由外到内执行,先执行Container的析构函数,再执行Component的析构函数,代码类似于

Container::~Container(...){...; ~Component(); }

5.3 委托Delegation

一个类中含有另一个类的指针,即Composition by reference。用空心菱形箭头指向被包含者表示两者之间关系。

5.4 继承Inheritance

继承表示子类继承自父类,是is-a的关系。用空心箭头从子类指向父类来表示继承关系。

父类的析构函数dtor必须定义成virtual,防止对子类析构时报错

继承关系下的构造函数执行过程:

构造由内到外:即先执行基类的构造函数,然后再执行自己。代码类似于:

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

析构由外而内:即先执行子类的析构函数,然后再执行自己的。代码类似于:

Derived::~Derived(...){...; ~Base(); }

5.5 继承与虚函数

Derived类继承Base类时,Base类中函数定义有如下情况:

  • non-virtual函数,不希望Derived类来重新写函数来覆盖它。
  • virtual函数,希望Derived类重新写函数定义它,并且Base类中该函数已有定义。
  • pure virtual函数,Derived类一定要重新定义实现它,并且Base类中没有该函数定义。

       

5.5.1 TemplateMethod示例

如下,myDoc对象将调用父类CDocument::OnFileOpen()函数,在该函数中传入指针为&myDoc。执行到Serialize()时查询myDoc的虚函数表vfunc table,调用自己的Serialize()。执行完自己Serialize()后返回继续执行OnFileOpen()。

#include <iostream>
using namespace std;

class CDocument
{
public:
    void OnFileOpen()
    {
        cout << "dialog..." << endl;
        cout << "check file status..." << endl;
        cout << "open file..." << endl;
        Serialize();
        cout << "close file..." << endl;
        cout << "update all views..." << endl;
    }

    virtual void Serialize() {};
};

class MyDoc: public CDocument
{
public:
    virtual void Serialize()
    {
        cout << "MyDoc::Serialize()..." << endl;
    }
};

int main()
{
    MyDoc myDoc;
    myDoc.OnFileOpen();

    return 0;
}


//output:
$ g++ -o TemplateMethod ./TemplateMethod.cpp
$ ./TemplateMethod
dialog...
check file status...
open file...
MyDoc::Serialize()...
close file...
update all views...

5.6 继承与复合(Inheritance+Composition)

继承和复合关系下的构造和析构:

1. 第一种关系如下:

构造时,Devired先调用Base类的default构造函数,然后再调用Component的构造函数,最后执行自己的操作。代码类似于:

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

析构时,先执行自己的析构,然后再析构Base和Component。代码类似于:

Derived::~Derived{ ...; ~Component(); ~Base() }

2.第二种关系如下:

构造时由内而外,Derived先调用Component的构造函数,再调用Base类构造函数,最后执行自己的构造操作。

析构时由外而内,Derived先执行自己的析构操作,再调用Base类析构函数,最后调用Component类析构函数。

5.7 继承与委托(Inheritance+Delegation)

继承+委托可以使用多态特性,符合面向对象中的“优先使用组合而非继承”原则, 是设计模式中常见方法。

5.7.1 Observer模式

5.7.2 Composite模式

例如文件系统中,文件和目录的表示可以采用Composite模式。此时Primitive类可以代表文件,Composite类可以用来表示目录。

#include <vector>

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

private:
    int value;
};


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


class Composite: public Component
{
public:
    Composite(int val): Component(val) {}

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

private:
    vector<Component *> c;
};

5.7.3 Prototype模式

#include <iostream>

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

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

class LandSatImage:: public Image
{
public:
    imageType returnType()
    {
        return LSAT;
    }

    void draw()
    {
        cout << "LandSatImage::draw()" << 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;

class SpotImage: public Image
{
public:
    imageType returnType()
    {
        return SPOT;
    }

    void draw()
    {
        cout << "SpotImage::draw()" << endl;
    }

    Image *clone()
    {
        return new SpotImage(1);
    }

protected:
    SpotImage(int dummy)
    {
        _id = _count++;
    }

private:
    static SpotImage _spotImage;
    SpotImage()
    {
        addPrototype(this);
    }
    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(i = 0; i < NUM_IMAGES; i++)
    {
        images[i]->draw();
    }
    //free the dynamic memory
    for(i = 0; i < NUM_IMAGES; i++)
    {
        delete images[i];
    }
    return 0;
}

6. 补充内容

6.1 static关键字

6.2 类模板和函数模板

6.3 namespace

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值