IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ Primer Plus 学习笔记(九) -> 正文阅读

[C++知识库]C++ Primer Plus 学习笔记(九)

第12章 类和动态内存分配

1. 动态内存和类

当类中有静态成员,则所有该类对象共享一个该类成员

class StringBad
{
private:
    char * str;
    int len;
    static int num_strings;
public:
    StringBad(const char * s);
    StringBad();
    ~StringBad();
    friend std::ostream & operator<<(std::ostream & os,
                                    const StringBad & st);
};

若创建了10个StringBad对象,则有10个 str 和 len,只有一个 num_strings。有一个对象更改该静态变量,所有其他对象的该变量都会改变。不能在类声明时初始化静态变量,这是因为会在类声明时分配内存,不过C++11可以类内初始化了...

静态成员一般在类方法文件中初始化:

int StringBad:: num_strings = 0; // 省略关键字 static

在没有显示定义一些方法时, c++ 会自动生成一些成员函数,包括:默认构造函数,默认析构函数,复制(拷贝)构造函数,赋值运算符重载,地址运算符重载。隐式的地址运算符返回调用的对象的地址(即this指针)。c++ 11 有额外提供了移动构造函数和移动赋值运算两个方法。

默认构造函数的定义没有任何操作

Klun::Klun()
{}

它跟声明一个未初始化的变量的行为是一样的,该类对象成员的值在这种情况下时是未知的。

而一旦在类声明中显示声明了带参数的构造函数,则c++ 不会再生成默认构造函数,如需要默认构造函数,需要自己定义。带参数的构造函数,可以通过默认参数的形式,定义一个默认构造函数,但是它与不带参数的默认构造函数冲突,只能有一个。

Klun::Klun(int n = 0) { ct = 0;}  
Klun::Klun() {ct = 0;}  // 只能有一个,会报错

复制构造函数的参数是类对象,用于初始化类对象,而不是类对象间赋值。

StringBad::StringBad(const StringBad &)
{
    ...
}
StringBad ditto;        // 调用默认构造函数
StringBad mtto(ditto);  // 调用复制构造函数初始化 mtto
StringBad also = mtto;  // 调用复制构造函数初始化 also
StringBad tee = StringBad(ditto);  // 调用复制构造函数
StringBad * pstr = new StringBad(mtto);  // 调用复制构造函数

中间两种声明可能会直接调用复制构造函数初始化mtto和also,也可能通过复制构造函数像创建临时对象,然后通过赋值运算符重载赋值。最后一种,会初始化一个匿名对象,然后将该对象的地址赋值给pstring指针。

默认的复制构造函数会逐个复制非静态成员的值。生成临时对象时,也会调用复制构造函数,对于类构造函数中使用 new 的情况下,需要显示定义一个复制构造函数,来避免因创建类临时对象产生的内存泄漏问题,显示定义的复制构造函数也就是深度复制。

StringBad::StringBad(const char * s)
{
    len = strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    num_strings++;
}
StringBad::~StringBad()
{
    num_strings--;
    delete [] str;
}

当没显示定义复制构造函数时,当创建一个类对象的临时对象时(如按值将类对象作为函数参数),临时对象出作用域时会自动调用析构函数,这样原对象的 str 指针和临时对象的 str 指针指向相同的 new 地址,指向的 new 地址就会泄漏。复制构造函数不仅复制数据地址,也要复制数据。

StringBad::StringBad(const StringBad & s)  // 显示定义复制构造函数,
{                                          // str 另 new 一块内存
    num_strings++;
    len = s.len;
    str = new char [len + 1];
    std::strcpy(str, s.str);
}

默认的赋值运算符重载同默认复制构造函数类似,逐值赋值非静态类成员,同样对于类构造函数中使用 new 的情况下,需要显示定义赋值运算符的重载,来防止 new 的内存泄漏。

StringBad & StringBad::operator=(const StringBad & st)
{
    if (this == & st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char [len + 1];
    std::strcpy(str, st.str);
    return *this;
}

析构函数中 delete 的形式要和构造函数中 new 的形式对应,即 new int [],则 delete [];new int, 则 delete 。

// 当析构函数是 delete [] 时,构造函数可以这么定义
String::String()
{
    len = 0;
    str = new char [1];
    str[0] = '/0';
}

delet 或者 delete [] 都可以作用于空指针,因此上面的定义等价于:

String::String()
{
    len = 0;
    str = nullptr;
}

可以将类成员函数声明为静态的(声明时必须包含关键字 static,如果是单独定义的,则定义时不能包含 static),不能通过类对象调用静态成员函数,实际上,静态成员函数不能使用 this 指针。若静态成员函数是公有的,可以通过类名和作用域解析符来调用它。静态成员函数只能只能使用类的静态成员,即 StringBad 类对象的静态成员函数只能使用 num_strings,而不能使用 str 和 len。

static int how() {return num_strings;}
cout << StringBad::how() << endl;  // 通过类名和::调用它

2. 使用指向对象的指针

使用指向类对象的指针,如:

StringBad s;
StringBad *pt = new StringBad(s);

将调用复制构造函数初始化一个未命名的类对象,pt 指向这个对象的地址

StringBad * pt = new StringBad;

将调用默认构造函数。

在使用指向对象的指针时,new 分配的内存负责储存 str 和 len的值,str 指向的字符串由构造函数分配内存,当类对象出作用域时,调用析构函数会 delete str 指向的字符串内存,因此还需要? delet pt;来释放类对象的内存(不带 [] ),当对象是通过 new 创建的,只有当显式调用 delete pt 时,才会调用该对象的析构函数。调用类方法时,使用 “ ->" 运算符代替 " . "。

当使用定位 new 运算符时创建对象时,会产生覆盖的情况,在构造函数有 new 的情况会产生问题:

char * buf = new char[512];
StringBad *pc1 = new (buf) StringBad;
StringBad *pc2 = new (buf) StringBad;  // pc2指向的对象会覆盖pc1指向的对象
                                       // 都是从buf[0]开始存储 

当 delete [] buf 时,也不会为定位 new 运算符创建的对象调用析构函数。因此,在使用定位 new 运算符连续创建对象时,要提供不同的存储位置:

pc1 = new (buf) StringBad;
pc2 = new (buf + sizeof(StringBad)) StringBad; // 使pc2的对象存储在pc1对象的后面

在使用 定位 new 运算符创建对象时,无法这样释放对象:delete pc2;这是因为pc2并没有收到 new 返回的地址,而当 delete pc1;时,也只会释放 buf 指向的那块512字节的内存块,这是因为pc1 与 buf 的地址相同。因此,对于定位 new 运算符创建对象的情况,要显示的调用析构函数来释放对象:

pc2->~StringBad();
pc1->~StringBad();
delete [] buf;

3. 队列模拟

在类中嵌套结构或类声明,声明的作用域是整个类,不会与其他类或者全局等同名声明冲突:

class Queue
{
    struct Node { Item item; struct * node;};
    enum { Q_SIZE = 10 };
    Node * front;
    const int qsize;
    ...
}

如果 Node 声明在公有部分,可以在类的外部,通过作用域解析运算符使用这个声明的类型:Queue::Node node;。

当 Queue 类的公有方法为私有的变量赋值时,qsize 会出现问题,它是 const 修饰的常量,常量只能初始化,而无法先声明再赋值。在调用构造函数时,如有一个构造函数 Queue(int qs) { qsize = qs;},对象将在代码执行前就被创建,这时候在为 qsize 赋值就会报错了。C++ 采用成员初始化列表解决这个问题:

Queue::Queue(int qs) : qsize(qs)
{
    front = rear = nullptr;
    items = 0;
}

这种方法不局限于初始化常量:

Queue::Queue(int qs) : qsize(qs), front(nullptr), rear(nullptr),
                       items(0)
{
}

只有构造函数可以使用成员初始化列表的方式,类中有 const 成员或者引用成员的时候,必须使用这种方法,引用也只能初始化,无法先声明后赋值。

C++ 11 开始允许在类内初始化变量:

class Classy
{
private:
    int mem = 1;
    const int mem2 = 20;
    ...
};

这与在构造函数中使用成员初始化列表等价。

数据成员被初始化的顺序与它们在类内声明的顺序相同,与初始化器的排列顺序无关。

?

?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 18:48:02  更:2022-06-29 18:49:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/19 22:02:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码