首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++基础-类和对象

C++基础-类和对象

作者头像
叶余
发布2019-07-01 14:00:43
9360
发布2019-07-01 14:00:43
举报

本文为 C++ 学习笔记,参考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代码大全》第 2 版。

面向对象编程有四个重要的基础概念:抽象封装继承多态。本文整理 C++ 中类与对象的基础内容,涉及抽象封装两个概念。《C++基础-继承》一文讲述继承概念。《C++基础-多态》一文讲述多态概念。这些内容是 C++ 中最核心的内容。

抽象

抽象是一种忽略个性细节、提取共性特征的过程。当用“房子”指代由玻璃、混凝土、木材组成的建筑物时就是在使用抽象。当把鸟、鱼、老虎等称作“动物”时,也是在使用抽象。

基类是一种抽象,可以让用户关注派生类的共同特性而忽略各派生类的细节。类也是一种抽象,用户可以关注类的接口本身而忽视类的内部工作方式。函数接口、子系统接口都是抽象,各自位于不同的抽象层次,不同的抽象层次关注不同的内容。

抽象能使人以一种简化的观点来考虑复杂的概念,忽略繁琐的细节能大大降低思维及实现的复杂度。如果我们在看电视前要去关注塑料分子、琉璃分子、金属原子是如何组成一部电视机的、电与磁的原理是什么、图像是如何产生的,那这个电视不用看了。我们只是要用一台电视,而不关心它是怎么实现的。同理,软件设计中,如果不使用各种抽象层次,那么这一堆代码将变得无法理解无法维护甚至根本无法设计出来。

封装

抽象是从一种高层的视角来看待一个对象。而封装则是,除了那个抽象的简化视图外,不能让你看到任何其他细节。简言之,封装就是隐藏实现细节,只让你看到想给你看的。

在程序设计中,就是把类的成员(属性和行为)进行整合和分类,确定哪些成员是私有的,哪些成员是公共的,私有成员隐藏,公共成员开放。类的用户(调用者)只能访问类的公共接口。

1. 类与对象

// 类:人类
class Human
{
pubilc:
    // 成员方法:
    void Talk(string textToTalk);   // 说话
    void IntroduceSelf();           // 自我介绍

private:
    // 成员属性:
    string name;                    // 姓名
    string dateOfBirth;             // 生日
    string placeOfBirth;            // 出生地
    string gender;                  // 性别

    ...
};

// 对象:具体的某个人
Human xiaoMing;
Human xiaoFang;

对象是类的实例。语句 Human xiaoMing;int a; 本质上并无不同,对象和类的关系,等同于变量和类型的关系。

不介意外部知道信息使用 public 关键字限定,需要保密的信息使用 private 关键字限定。

2. 构造函数

2.1 构造函数

构造函数在创建对象时被调用。执行初始化操作。

  1. 构造函数名字与类名相同
  2. 构造函数无返回值
  3. 构造函数可以重载,一个类可有多个构造函数
  4. 构造函数不能被声明为 const,因为一个 const 对象也是通过构造函数完成初始化的,构造函数完成初始化之后,const 对象才真正取得"常量"属性。

构造函数形式如下:

class Human
{
public:
    Human();    // 构造函数声明
};

Human::Human()  // 构造函数实现(定义)
{
    ...
}

2.2 默认构造函数

可不提供实参调用的构造函数是默认构造函数,包括如下两种: 1) 不带任何函数形参的构造函数是默认构造函数 2) 带有形参但所有形参都提供默认值的构造函数也是默认构造函数,因为这种既可以携带实参调用,也可以不带实参调用

2.3 合成的默认构造函数

当用户未给出任何构造函数时,编译器会自动生成一个构造函数,叫作合成的默认构造函数,此函数对类的数据成员初始化规则如下: 1) 若数据成员存在类内初始化值,则用这个初始化值来初始化数据成员 2) 否则,执行默认初始化。默认值由数据类型确定。参"C++ Primer 5th"第 40 页

下面这个类因为没有任何构造函数,所以编译器会生成合成的默认构造函数:

class Human
{
pubilc:
    // 成员方法:
    void Talk(string textToTalk);   // 说话
    void IntroduceSelf();           // 自我介绍

private:
    // 成员属性:
    string name;                    // 姓名
    string dateOfBirth;             // 生日
    string placeOfBirth;            // 出生地
    string gender;                  // 性别
};

2.4 参数带默认值的构造函数

函数可以有带默认值的参数,构造函数当然也可以。

class Human
{
private:
    string name;
    int age;

public:
    // overloaded constructor (no default constructor)
    Human(string humansName, int humansAge = 25)
    {
        name = humansName;
        age = humansAge;
        ...
    };

可以使用如下形式的实例化

Human adam("Adam"); // adam.age is assigned a default value 25
Human eve("Eve, 18); // eve.age is assigned 18 as specified

2.5 带初始化列表的构造函数

初始化列表是一种简写形式,将相关数据成员的初始化列表写在函数名括号后,从而可以省略函数体中的相应数据成员赋值语句。

Human::Human(string humansName, int humansAge) : name(humansName), age(humansAge)
{
}

上面这种写法和下面这种写法具有同样的效果:

Human::Human(string humansName, int humansAge)
{
    name = humansName; 
    age = humansAge;
}

2.6 复制构造函数

2.6.1 浅复制及其问题

复制一个类的对象时,只复制其指针成员但不复制指针指向的缓冲区,其结果是两个对象指向同一块动态分配的内存。销毁其中一个对象时,delete[] 释放这个内存块,导致另一个对象存储的指针拷贝无效。这种复制被称为浅复制。

如下为浅复制的一个示例程序:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
    char* buffer;

public:
    MyString(const char* initString)
    {
        buffer = NULL;
        if(initString != NULL)
        {
            buffer = new char [strlen(initString) + 1];
            strcpy(buffer, initString);
        }
    }

    ~MyString()
    {
        cout << "Invoking destructor, clearing up" << endl;
        delete [] buffer;
    }

    int GetLength() { return strlen(buffer); }
    const char* GetString() { return buffer; }
};

void UseMyString(MyString str)
{
    cout << "String buffer in MyString is " << str.GetLength();
    cout << " characters long" << endl;

    cout << "buffer contains: " << str.GetString() << endl;
    return;
}

int main()
{
    MyString sayHello("Hello from String Class");
    UseMyString(sayHello);

    return 0;
}

分析一下 UseMyString(sayHello); 这一语句:

  1. 执行对象浅复制,将实参 sayHello 复制给形参 str,复制了数据成员(指针)的值,但未复制成员指向的缓冲区。因此两份对象拷贝的指针数据成员(char *buffer)指向同一内存区。
  2. UseMyString() 返回时,str 析构,内存区被回收
  3. main() 返回时,sayHello 析构,再次回收内存区,导致段错误
2.6.2 复制构造函数:确保深复制

复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将调用复制构造函数。

复制构造函数函数语法如下:

class MyString
{
    MyString(const MyString& copySource); // copy constructor
};

MyString::MyString(const MyString& copySource)
{
    // Copy constructor implementation code
}

复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,您使用它来编写自定义的复制代码,确保对所有缓冲区进行深复制。

复制构造函数的参数必须按引用传递,否则复制构造函数将不断调用自己,直到耗尽系统的内存为止。原因就是每当对象被复制时,编译器都将调用复制构造函数,如果参数不是引用,实参不断复制给形参,将生成不断复制不断调用复制构造函数。

示例程序如下:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
    char* buffer;

public:
    MyString() {}
    MyString(const char* initString) // constructor
    {
        buffer = NULL;
        cout << "Default constructor: creating new MyString" << endl;
        if(initString != NULL)
        {
            buffer = new char [strlen(initString) + 1];
            strcpy(buffer, initString);

            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }

    MyString(const MyString& copySource) // Copy constructor
    {
        buffer = NULL;
        cout << "Copy constructor: copying from MyString" << endl;
        if(copySource.buffer != NULL)
        {
            // allocate own buffer 
            buffer = new char [strlen(copySource.buffer) + 1];

            // deep copy from the source into local buffer
            strcpy(buffer, copySource.buffer);

            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }

    // Destructor
    ~MyString()
    {
        cout << "Invoking destructor, clearing up" << endl;
        delete [] buffer;
    }

    int GetLength() 
    { return strlen(buffer); }

    const char* GetString()
    { return buffer; }
};

void UseMyString(MyString str)
{
    cout << "String buffer in MyString is " << str.GetLength();
    cout << " characters long" << endl;

    cout << "buffer contains: " << str.GetString() << endl;
    return;
}

int main()
{
    MyString sayHello("Hello from String Class");
    UseMyString(sayHello);

    return 0;
}

再看 UseMyString(sayHello); 这一语句:

  1. 每当对象被复制时,编译器都将调用复制构造函数。将实参复制给形参时,编译器就会调用复制构造函数。
  2. 所以这里的 str 是通过调用复制构造函数进行的初始化,对实参进行了深复制。形参与实参中的指针成员各指向自己的缓冲区。
  3. 所以析构是正常的,示例程序运行没有问题。

同样,如果没有提供复制赋值运算符 operator=,编译器提供的默认复制赋值运算符将导致浅复制。

关于复制构造函数的注意事项如下:

  1. 类包含原始指针成员(char *等)时,务必编写复制构造函数和复制赋值运算符。
  2. 编写复制构造函数时,务必将接受源对象的参数声明为 const 引用。
  3. 声明构造函数时务必考虑使用关键字 explicit,以避免隐式转换。
  4. 务必将类成员声明为 std::string 和智能指针类(而不是原始指针),因为它们实现了复制构造函数,可减少您的工作量。除非万不得已,不要类成员声明为原始指针。
2.6.3 移动构造函数:改善性能
class MyString
{
    // 代码同上一示例程序,此处略
};

MyString Copy(MyString& source)
{
    MyString copyForReturn(source.GetString()); // create copy
    return copyForReturn;                       // 1. 将返回值复制给调用者,首次调用复制构造函数
}

int main()
{
    MyString sayHello ("Hello World of C++");
    MyString sayHelloAgain(Copy(sayHello));     // 2. 将 Copy() 返回值作实参,再次调用复制构造函数
    return 0;
}

上例中,参考注释,实例化 sayHelloAgain 对象时,复制构造函数被调用了两次。如果对象很大,两次复制造成的性能影响不容忽视。

为避免这种性能瓶颈, C++11 引入了移动构造函数。移动构造函数的语法如下:

// move constructor
MyString(MyString&& moveSource)
{
    if(moveSource.buffer != NULL)
    {
        buffer = moveSource.buffer;     // take ownership i.e. 'move'
        moveSource.buffer = NULL;       // set the move source to NULL
    }
}

有移动构造函数时,编译器将自动使用它来“移动”临时资源,从而避免深复制。增加移动构造函数后,上一示例中,将首先调用移动构造函数,然后调用复制构造函数,复制构造函数只被会调用一次。

3. 析构函数

析构函数在对象销毁时被调用。执行去初始化操作。

  1. 析构函数只能有一个,不能被重载。
  2. 若用户未提供析构函数,编译器会生成一个伪析构函数,但是这个伪析构函数是空的,不会释放堆内存。

每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。

4. 构造函数与析构函数的其他用途

4.1 不允许复制的类

假设要模拟国家政体,一个国家只能一位总统,President 类的对象不允许复制。

要禁止类对象被复制,可将复制构造函数声明为私有的。为禁止赋值,可将赋值运算符声明为私有的。复制构造函数和赋值运算符声明为私有的即可,不需要实现。这样,如果代码中有对对象的复制或赋值,将无法编译通过。形式如下:

class President
{
private:
    President(const President&);              // private copy constructor
    President& operator= (const President&);  // private copy assignment operator
    // … other attributes
};

4.2 只能有一个实例的单例类

前面讨论的 President 不能复制,不能赋值,但存在一个缺陷:无法禁止通过实例化多个对象来创建多名总统:

President One, Two, Three;

要确保一个类不能有多个实例,也就是单例的概念。实现单例,要使用私有构造函数、私有赋值运算符和静态实例成员。

将关键字 static 用于类的数据成员时,该数据成员将在所有实例之间共享。 将关键字 static 用于成员函数(方法)时,该方法将在所有成员之间共享。 将 static 用于函数中声明的局部变量时,该变量的值将在两次调用之间保持不变。

4.3 禁止在栈中实例化的类

将析构函数声明为私有的。略

4.4 使用构造函数进行类型转换

5. this 指针

在类中,关键字 this 包含当前对象的地址,换句话说, 其值为&object。在类成员方法中调用其他成员方法时, 编译器将隐式地传递 this 指针。

调用静态方法时,不会隐式地传递 this 指针,因为静态函数不与类实例相关联,而由所有实例共享。要在静态函数中使用实例变量,应显式地声明一个形参,并将实参设置为 this 指针。

6. sizeof 用于类

sizeof 用于类时,值为类声明中所有数据属性占用的总内存量,单位为字节。是否考虑对齐,与编译器有关。

7. 结构与类的不同之处

结构 struct 与类 class 非常相似,差别在于程序员未指定时,默认的访问限定符(public 和 private)不同。因此,除非指定了,否则结构中的成员默认为公有的(而类成员默认为私有的);另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 类与对象
  • 2. 构造函数
    • 2.1 构造函数
      • 2.2 默认构造函数
        • 2.3 合成的默认构造函数
          • 2.4 参数带默认值的构造函数
            • 2.5 带初始化列表的构造函数
              • 2.6 复制构造函数
                • 2.6.1 浅复制及其问题
                • 2.6.2 复制构造函数:确保深复制
                • 2.6.3 移动构造函数:改善性能
            • 3. 析构函数
            • 4. 构造函数与析构函数的其他用途
              • 4.1 不允许复制的类
                • 4.2 只能有一个实例的单例类
                  • 4.3 禁止在栈中实例化的类
                    • 4.4 使用构造函数进行类型转换
                    • 5. this 指针
                    • 6. sizeof 用于类
                    • 7. 结构与类的不同之处
                    相关产品与服务
                    对象存储
                    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档