C++ Class笔记

简单记下关于C++类一些笔记。


构造、析构、 拷贝构造、赋值

结构体(struct)

说类(class)之前,说下结构体。在C++中,结构体是一种特殊形态的类,区别如下:
结构体和类的唯一区别就是,结构体和类具有不同的默认访问控制属性。

  • class: 对于未指定访问控制属性的成员,其默认属性为private;
  • struct: 对于未指定任何访问控制属性的成员,其默认属性为public;

在C++中,结构体同样具有构造函数、析构函数、拷贝构造函数、赋值函数等函数。而且,类(class)还可以与结构体(struct)相互继承。

下面以一个结构体为例,来简单记下构造等函数的基本写法:

struct DataPack
{
    int size;
    char* name;
};

构造函数与析构函数

如果没有定义构造函数与析构函数,C++则会定义默认的构造函数析构函数,默认的形式如下:

struct DataPack
{
    int size;
    char* name;

    DataPack(){}
    ~DataPack(){}
};
  • 构造函数用于在创建对象时,给对象的成员进行赋值,不需要赋值用默认的即可;
  • 析构函数用于在消毁对象时,释放对象中指针成员指向的内存;

拷贝构造函数与赋值函数

同样,C++会定义默认的拷贝构造函数和赋值函数,如果没有的话。正因为有默认的,以下一般性的代码才可以执行:

DataPack pack;
DataPack newpack1 = pack;       // 对象初始化,调用拷贝构造函数
DataPack newpack2(pack);        // 对象初始化,调用拷贝构造函数
DataPack newpack3;
newpack3 = pack;                // 对象赋值,调用赋值函数(operator)

但默认的拷贝构造函数和赋值函数是浅拷贝,即将两个对象对应的成员进行简单的赋值,若是有指针成员,在对象析构时会发生错误,拿指针来说,两个对象经的指针指向同一块内存,析构时,这块内存就会被delete两次,这明显不行。所有就需要深拷贝了,如下所示:

struct DataPack
{
    int size;
    char* name;

    // 浅拷贝构造函数
    DataPack(const DataPack& dp){
        this->size = dp.size;
        this->name = dp.name;
    }
    // 深拷贝构造函数
    DataPack(const DataPack& dp){
        this->size = dp.size;
        this->name = new char[this->size];
        memcpy(this->name, dp.name, this->size);
    }

    // 浅拷贝赋值函数
    DataPack& operator=(const DataPack& dp){
        this->size = dp.size;
        this->name = dp.name;
        return *this;
    }
    // 深拷贝赋值函数
    DataPack& operator=(const DataPack& dp){
        this->size = dp.size;
        delete this->name;              // 先释放原来的内存
        this->name = new char[this->size];
        memcpy(this->name, dp.name, this->size);
        return *this;
    }
};

拷贝构造函数的几点说明

  • 默认拷贝构造函数没有处理static数据成员;
  • 拷贝构造函数必须是引用传递,如果是值传递,则会无限的调用拷贝构造函数,因为值传递本身就是先要调用拷贝构造函数;如果是指针传递,则只是构造函数,而不是拷贝构造函数;
  • 拷贝构造函数中不受private限制,即可以直接访问private成员变量;
  • 对于一个类X, 如果一个构造函数的第一个参数是下列之一,且没有其他参数,或其他参数都有默认值,那么这个函数是拷贝构造函数.
(1) X&  (2) const X&  (3) volatile X&  (4) const volatile X&

赋值函数(operator=)的几点说明

  • 赋值函数的前提是对象已经初始化,所在赋值函数中,对象先会丢弃原有的值(指针则先要释放内存),再赋予新的值(指针则重新申请内存);

public, protected, private与继承

访问权限区别

访问属性 说明
public 可以被对象实体直接访问
protected 只允许在本类和子类中访问,对象实体通过public员函数访问
private 只允许在本类中访问,对象实体通过public员函数访问

继承方式与权限的关系

基类访问属性 继承方式 子类访问属性
public public public
public protected protected
public private private
protected public protected
protected protected protected
protected private private
private public,protectd,private 子类无权访问

基类指针指向派生类实例的原理

简单的用图来解释下。基类指针可访问的内存地址长度,比派生类指针可访问的内存地址长度要短。C++中可以用基类指针指向派生类实例,但base_ptr可访问的地址长度只限于BaseClass范围,如下图所示;反之,用派生类指针指向基类实例则是不允许的,因为derived_ptr访问BaseClass范围之外的地址时,则会发生指针越界


base_ptr |---------------|---------------| derived_ptr
      |  |               |               |   |
      |  |Base class     | Derived Class |   |
      |  |               |               |   |
      -  |---------------|               |   |
                         |               |   |
                         |               |   |
                         |               |   |
                         |---------------|   -

C++异常处理中的栈回退机制(stack unwind)

C++异常机制的实现方式和开销分析

异常处理涉及了函数之间的跳转(goto只是函数局部调转),比如c中用的setjump、longjump。

C++中的异常处理,是在函数调用堆栈中添加了栈回退机制。

  • 栈回退需要释放throw内声明的临时变量等资源;
  • 栈回退需要在当前函数中查找try对应的catch代码块,若未找到,则回退到上一级函数继续查找;
try {
  func();           // 栈回退实现了try中catch到func内部的throw
} catch(){...}

void func() {
  ClassA a;
  ClassB b;

  throw();          // throw后,需要释放a和b,故出现异常时,通过栈回退来调用a和b的析构函数;
                    // 若在异常,析构函数中再次引发异常,则会强行停止进程;
                    // 故在析构函数中不应该throw,即使throw也最好在析构函数内部就catch并处理;

  ClassC c;
}
#include <iostream>

class A {
    ~A() {
        // throw 1;     // 会在异常中再次引发引常
        try {
            throw 1;
        } catch (int t) {
            std::cout << t << std::endl;
        }
    }
};

void func() {
    A a();
    throw 2;
}

int main(void)
{
    try {
        func();
    } catch (int t) {
        std::cout << t << std::endl;
    }
    return 0;
}

C++异常机制的实现方式和开销分析

异常处理涉及了函数之间的跳转(goto只是函数局部调转),比如c中用的setjump、longjump。

C++中的异常处理,是在函数调用堆栈中添加了栈回退机制。

  • 栈回退需要释放throw内声明的临时变量等资源;
  • 栈回退需要在当前函数中查找try对应的catch代码块,若未找到,则回退到上一级函数继续查找;
try {
  func();           // 栈回退实现了try中catch到func内部的throw
} catch(){...}

void func() {
  ClassA a;
  ClassB b;

  throw();          // throw后,需要释放a和b,故出现异常时,通过栈回退来调用a和b的析构函数;
                    // 若在异常,析构函数中再次引发异常,则会强行停止进程;
                    // 故在析构函数中不应该throw,即使throw也最好在析构函数内部就catch并处理;

  ClassC c;
}
#include <iostream>

class A {
    ~A() {
        // throw 1;     // 会在异常中再次引发引常
        try {
            throw 1;
        } catch (int t) {
            std::cout << t << std::endl;
        }
    }
};

void func() {
    A a();
    throw 2;
}

int main(void)
{
    try {
        func();
    } catch (int t) {
        std::cout << t << std::endl;
    }
    return 0;
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]

文章标题:C++ Class笔记

本文作者:Y

发布时间:2017-09-29, 21:50:32

最后更新:2020-07-16, 16:28:09

原始链接:http://yehuohan.github.io/2017/09/29/%E6%9D%82%E8%AE%B0/Cpp%E7%B1%BB%E7%AC%94%E8%AE%B0/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。