C++入门——01类与对象

1.类

1.1.类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

struct Student
{
    void SetStudentInfo(const char* name, const char* gender, int age)
    {
        strcpy(_name, name);
        strcpy(_gender, gender);
        _age = age;
    }
    void PrintStudentInfo()
    {
        cout<<_name<<" "<<_gender<<" "<<_age<<endl;
    }
    char _name[20];
    char _gender[3];
    int _age;
};

int main()
{
    Student s;
    s.SetStudentInfo("Peter", "男", 18);
    return 0;
}

上面结构体的定义,在C++中更喜欢用class来代替

1.2类的定义

class className
{
    // 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号


类的两种定义方式:

  • 1.声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
// MyClass.h
class MyClass {
private:
    int value;

public:
    MyClass(int v) : value(v) {}  // 构造函数的定义

    void setValue(int v) {  // 成员函数的定义
        value = v;
    }

    int getValue() const {  // 成员函数的定义
        return value;
    }
};
  • 2. 声明放在.h文件中,类的定义放在.cpp文件中

头文件:MyClass.h

#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
private:
    int value;

public:
    MyClass(int v);  // 构造函数的声明
    void setValue(int v);  // 成员函数的声明
    int getValue() const;  // 成员函数的声明
};

#endif

源文件:MyClass.cpp

#include "MyClass.h"

MyClass::MyClass(int v) : value(v) {}  // 构造函数的定义

void MyClass::setValue(int v) {  // 成员函数的定义
    value = v;
}

int MyClass::getValue() const {  // 成员函数的定义
    return value;
}

1.3 类的访问限定符

在 C++ 中,类的访问限定符(access specifiers)用于控制类成员的访问权限。访问限定符帮助实现封装(encapsulation),一种将数据和操作数据的方法封装在一起的机制,从而保护对象的内部状态不被外部直接修改。

C++ 提供了三种主要的访问限定符:

  1. public:公有成员

    • 任何地方的代码都可以访问公有成员。公有成员通常是类的接口,允许外部代码使用这些方法和属性。
  2. private:私有成员

    • 只有类内部的成员函数和友元函数可以访问私有成员。私有成员用于隐藏类的内部实现细节,以保护数据不被外部直接修改。
  3. protected:受保护成员

    • 受保护成员在类内部和继承的子类中可以访问,但在类外部无法直接访问(
      protected和private是类似的)。受保护成员用于允许派生类访问基类的部分实现,同时保护类外部的代码。

注意:class的默认访问权限为private,struct为public(因为struct要兼容C),访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

#include <iostream>
using namespace std;

class BankAccount {
private:
    double balance;  // 私有成员

public:
    // 公有构造函数
    BankAccount(double initialBalance) : balance(initialBalance) {}

    // 公有成员函数
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }

    double getBalance() const {
        return balance;
    }
};

class SavingsAccount : public BankAccount {
private:
    double interestRate;  // 私有成员

public:
    SavingsAccount(double initialBalance, double rate)
        : BankAccount(initialBalance), interestRate(rate) {}

    void addInterest() {
        double interest = getBalance() * interestRate;
        deposit(interest);  // 调用基类的公有函数
    }
};

int main() {
    BankAccount account(1000);
    account.deposit(500);
    account.withdraw(200);
    cout << "BankAccount Balance: " << account.getBalance() << endl;

    SavingsAccount savings(1000, 0.05);
    savings.addInterest();
    cout << "SavingsAccount Balance with Interest: " << savings.getBalance() << endl;

    return 0;
}

1.4类的封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。

1.5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

class Person
{
    public:
    void PrintPersonInfo();
    private:
    char _name[20];
    char _gender[3];
    int _age;
};

// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}

1.6.类的实例化

用类类型创建对象的过程,称为类的实例化
1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

1.7.类的大小

在 C++ 中,一个类的对象包含了类中定义的所有成员变量以及一些与对象相关的运行时信息。类的大小(即对象的内存占用)主要由其成员变量的类型和数量决定,同时还可能受到编译器对内存对齐和填充的影响。

1.7.1类的对象包含什么?

  1. 成员变量(属性)

    • 类中定义的所有数据成员(变量)。这些成员变量存储了类的状态。例如,如果类定义了 intdouble 类型的成员变量,那么对象就会包含这些变量。
  2. 成员函数(方法)

    • 成员函数定义了类的行为,但成员函数并不会直接存储在对象中。对象本身只存储成员函数的地址(即函数指针)用于调用。成员函数的代码只在类的定义中存在,实际的函数代码是在程序的代码段中。
  3. 对象的管理信息

    • 有些编译器可能会在对象中附加一些管理信息,如虚表指针(vtable pointer)用于支持虚函数的动态绑定。这通常发生在类有虚函数时。

1.7.2计算类的大小

类的大小由以下因素决定:

  1. 成员变量的类型和数量

    • 类中所有成员变量的总大小。不同类型的变量(如 intdoublechar)占用的内存不同。
  2. 内存对齐和填充

    • 为了提高内存访问效率,编译器会根据机器架构对对象进行对齐。编译器可能会在对象的成员之间或对象的末尾插入填充字节,以满足对齐要求。这会导致实际的对象大小大于成员变量的总大小。
  3. 虚表指针

    • 如果类包含虚函数(即类有虚函数表),每个对象会额外包含一个虚表指针(通常是一个指针大小的内存)。

// 类中既有成员变量,又有成员函数
class A1 {
    public:
    void f1(){}
    private:
    int _a;
};

// 类中仅有成员函数
class A2 {
    public:
    void f2() {}
};

// 类中什么都没有---空类
class A3
{};


sizeof(A1) : 8 sizeof(A2) : 1 sizeof(A3) : 1

对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8

计算示例类的大小

  • 一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
  • 成员函数本身不直接占用对象的内存,但为了实现对象的唯一性和方法的调用,可能会有额外的内部数据。
  • 虚表指针:如果类有虚函数,可能会有一个额外的虚表指针(通常是 4 或 8 字节)。

结构体内存对齐规则

1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
        VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

1.8this指针

class Date
{
public :
    void Display ()
    {
        cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    void SetDate(int year , int month , int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

int main()
{
    Date d1, d2;
    d1.SetDate(2018,5,1);
    d2.SetDate(2018,7,1);
    d1.Display();
    d2.Display();
    return 0;
}

Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

1.8.1 this指针的特性

  • this指针的类型:类型* const
  • 只能在“成员函数”的内部使用
  • this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  • this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需 要用户传递
     

1.8.2. this 指针存在哪里

在 C++ 中,this 指针是一个隐含的指针,指向当前对象的地址。this 指针在以下几种情况下存在:

  • 成员函数:当一个对象调用其成员函数时,this 指针作为隐式参数传递给该成员函数。它指向调用该函数的对象。

  • 对象方法调用时this 指针在成员函数的调用过程中由编译器自动传递,不需要显示传递或存储。它的值是在函数调用时由编译器和运行时环境确定的,存储在栈上,与函数的局部变量一起分配内存。

  • 对象的内存this 指针本身不是一个存储在固定位置的对象,而是指向一个具体对象的指针。它指向的对象可能存储在栈上(如果是局部对象),或者在堆上(如果是通过 new 创建的对象),或者在静态存储区(如果是静态对象)。

1.8.3. this 指针可以为空吗

在正常的成员函数调用中,this 指针不应为空。它总是指向调用该函数的对象。然而,this 指针在以下情况下可能会为空:

  • 静态成员函数:静态成员函数没有 this 指针,因为它们与任何特定的对象实例无关。它们可以通过类名直接调用,而不是通过对象实例调用。

  • 对象销毁后:如果成员函数在对象已经被销毁后被调用,this 指针会指向一个无效的内存位置。虽然 C++ 语言标准不允许使用销毁后的对象,但一些错误的代码可能在这种情况下出现未定义行为。

  • 某些特殊情况下:例如,强制将 this 指针设置为 nullptr,虽然这种操作是不推荐的,但编译器不会阻止这种行为。

#include <iostream>

class MyClass {
public:
    void nonStaticMemberFunction() {
        if (this == nullptr) {
            std::cout << "this is nullptr" << std::endl;
        } else {
            std::cout << "this is not nullptr" << std::endl;
        }
    }
    
    static void staticMemberFunction() {
        // 没有 this 指针
        std::cout << "Static function, no this pointer" << std::endl;
    }
};

int main() {
    MyClass obj;
    obj.nonStaticMemberFunction();  // this is not nullptr
    MyClass::staticMemberFunction(); // Static function, no this pointer

    return 0;
}

1.9. 类的六个成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

  • 默认构造函数(Default Constructor)

    • 如果用户没有提供任何构造函数,编译器会自动生成一个默认构造函数。这个函数没有参数,用来初始化对象的成员变量。
  • 析构函数(Destructor)

    • 当对象的生命周期结束时,编译器会自动调用析构函数来清理对象。默认情况下,析构函数不执行任何操作,但如果类包含指针或需要释放资源,通常需要自定义析构函数。
  • 拷贝构造函数(Copy Constructor)

    • 用于通过另一个同类型的对象来创建新对象。默认的拷贝构造函数会逐个复制对象的所有成员变量。
  • 拷贝赋值运算符(Copy Assignment Operator)

    • 用于将一个对象赋值给另一个同类型的对象。默认的拷贝赋值运算符也是逐个复制对象的所有成员变量。
  • 移动构造函数(Move Constructor)

    • 用于通过“移动”资源来构建新对象,而不是复制资源。默认情况下,编译器只会在满足某些条件时生成移动构造函数。
  • 移动赋值运算符(Move Assignment Operator)

    • 类似于拷贝赋值运算符,但通过“移动”资源而不是复制资源来赋值给对象。与移动构造函数一样,编译器会根据需要生成默认的移动赋值运算符。

1.9.1构造函数

class Date
{
public:
    void SetDate(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Display()
    {
        cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1,d2;
    d1.SetDate(2018,5,1);
    d1.Display();
    Date d2;
    d2.SetDate(2018,7,1);
    d2.Display();
    return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

1.9.1.1构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:
        1. 函数名与类名相同。
        2. 无返回值。
        3. 对象实例化时编译器自动调用对应的构造函数。
        4. 构造函数可以重载。

class Date
{
public :
    // 1.无参构造函数
    Date ()
    {}
    // 2.带参构造函数
    Date (int year, int month , int day )
    {
    _year = year ;
    _month = month ;
    _day = day ;
    }
private :
    int _year ;
    int _month ;
    int _day ;
};

void TestDate()
{
    Date d1; // 调用无参构造函数
    Date d2 (2015, 1, 1); // 调用带参的构造函数
    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    Date d3();
}

        5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:
    /*
    // 如果用户显式定义了构造函数,编译器将不再生成
    Date (int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    */
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
    Date d;
}

        6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

// 默认构造函数
class Date
{
public:
    Date()
    {
        _year = 1900 ;
        _month = 1 ;
        _day = 1;
    }
    
    Date (int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private :
    int _year ;
    int _month ;
    int _day ;
};


void Test()
{
    Date d1;  //调用默认构造函数(无参构造函数)
}

7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
    private:
    // 基本类型(内置类型)
    int _year;
    int _month;
    int _day;
    // 自定义类型
    Time _t;
};

int main()
{
    Date d;
    return 0;
}

8. 成员变量的命名风格(一般前面加_)

// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
    Date(int year)
    {
        // 这里的year到底是成员变量,还是函数形参?
        year = year;
    }

private:
    int year;

};

// 所以我们一般都建议这样
class Date
{
    public:
    Date(int year)
    {
        _year = year;
    }
private:
    int _year;
};

// 或者这样。
class Date
{
public:
    Date(int year)
    {
        m_year = year;
    }
private:
    int m_year;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。
1.9.1.2构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。这就需要初始化列表。

1.9.1.3初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
    Date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

【注意】
        1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
        2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
                引用成员变量
                const成员变量
                自定义类型成员(该类没有默认构造函数)

class A
{
public:
    A(int a):_a(a)
    {}
private:
    int _a;
};

class B
{
public:
    B(int a, int ref):_aobj(a),_ref(ref),_n(10)
    {}
private:
    A _aobj; // 没有默认构造函数
    int& _ref; // 引用
    const int _n; // const
};
  • 初始化顺序:成员变量按声明顺序初始化,不按初始化列表的顺序。
  • 没有默认构造函数的成员:必须在初始化列表中初始化。
  • 引用成员:必须在初始化列表中绑定到有效对象。
  • const 成员:必须在初始化列表中初始化。

        3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

class Time
{
public:
    Time(int hour = 0)
        :_hour(hour)
    {
        cout << "Time()" << endl;
    }
private:
    int _hour;
};

class Date
{
public:
    Date(int day)
    {}
private:
    int _day;
    Time _t;
};

int main()
{
    Date d(1);
}

        4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

#include <iostream>

class A
{
public:
    A(int a)
        : _a1(a)
        , _a2(_a1) // 这里 _a2 被初始化为 _a1 的值
    {}
    
    void Print() {
        std::cout << _a1 << " " << _a2 << std::endl;
    }

private:
    int _a2;
    int _a1;
};

int main() {
    A aa(1);
    aa.Print();  //1 随机值,先初始化a2,此时a1为随机值
    return 0;
}

1.9.2析构函数

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

1.9.2.1析构函数的特性

析构函数是特殊的成员函数。
其特征如下:
        1. 析构函数名是在类名前加上字符 ~。
        2. 无参数无返回值。
        3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
        4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

typedef int DataType;

class SeqList
{
public :
    SeqList (int capacity = 10)
    {
        _pData = (DataType*)malloc(capacity * sizeof(DataType));
        assert(_pData);
        _size = 0;
        _capacity = capacity;
     }
   
     ~SeqList()
    {
        if (_pData)
        {
            free(_pData ); // 释放堆上的空间
            _pData = NULL; // 将指针置为空
            _capacity = 0;
            _size = 0;
        }
    }

private :
    int* _pData ;
    size_t _size;
    size_t _capacity;
};

        5. 关于编译器自动生成的析构函数,对会自定类型成员调用它的析构函数。

class String
{
public:
    String(const char* str = "jack")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }
    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }

private:
    char* _str;
};

class Person
{
private:
    String _name;
    int _age;
};

int main()
{
    Person p;
    return 0;
}
  • main 函数中,声明了一个 Person 类型的对象 p
  • p 的生命周期结束时(即 main 函数结束时),p 的析构函数会被调用。因为 Person 类没有定义析构函数,编译器会自动生成一个默认析构函数。
  • 这个默认的析构函数会依次调用 Person 类成员变量的析构函数。对于 _nameString 类型的对象),它的析构函数会被调用,这会导致打印 ~String(),并释放 _str 指向的内存。

先是Person构造函数调用,然后String类构造函数调用,最后析构的时候,先析构String类,再析构Person类,先构造的后析构。

1.9.3拷贝构造函数

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

1.9.3.1 拷贝构造函数的特征

其特征如下:
        1. 拷贝构造函数是构造函数的一个重载形式。
        2. 拷贝构造函数的参数只有一个且必须使用引用传参使用传值方式会引发无穷递归调用

如果是传值,在传参过程中又要发生参数的拷贝,再次进行传值拷贝,一直这样拷贝下去。

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
    Date(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
}

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    Date d2(d1); 
    return 0;
}

        3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    // 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
    Date d2(d1);
    return 0;
}

        4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?浅拷贝可能导致多个对象共享相同的资源(如指针指向的动态内存)

// 这里会发现下面的程序会崩溃掉?这里就需要深拷贝去解决。

class String
{
public:
    String(const char* str = "jack")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }

    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }

private:
    char* _str;
};

int main()
{
    String s1("hello");
    String s2(s1);
}

1.9.4.赋值运算符重载

在赋值运算符重载前,我们要了解运算符重载,C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

1.9.4.1运算符重载

函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
// 全局的operator==

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

//private:
    int _year;
    int _month;
    int _day;
};

// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?
// 这里其实可以用友元解决,或者干脆重载成成员函数。

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year;
    && d1._month == d2._month
    && d1._day == d2._day;
}

void Test ()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout<<(d1 == d2)<<endl;
}
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
   
    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this指向的调用函数的对象
    
    bool operator==(const Date& d2)
    {
        return _year == d2._year;
        && _month == d2._month
        && _day == d2._day;
    }

private:
    int _year;
    int _month;
    int _day;
};

void Test ()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout<<(d1 == d2)<<endl;
}
1.9.4.2赋值运算符重载

赋值运算符主要特性:

        1. 参数类型
        2. 返回值
        3. 检测是否自己给自己赋值
        4. 返回*this

class Date
{
public :
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
   
     Date (const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    
    Date& operator=(const Date& d)
    {
        if(this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
    }

private:
    int _year ;
    int _month ;
    int _day ;
};

        5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    Date d2(2018,10, 1);
    // 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
    d1 = d2;
    return 0;
}

        6.编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,拷贝赋值操作符执行了浅拷贝,导致多个对象共享相同的内存资源。当其中一个对象释放这块内存时,另一个对象中的指针会变为悬空指针,这可能导致程序崩溃。

// 这里会发现下面的程序会崩溃掉?这里就需要深拷贝去解决。
class String
{
public:
    String(const char* str = "")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }
    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }

private:
    char* _str;
};

int main()
{
    String s1("hello");
    String s2("world");
    s1 = s2;
}

1.10const成员

1.10.1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

class Date
{
public :
    void Display ()
    {
        cout<<"Display ()" <<endl;
        cout<<"year:" <<_year<< endl;
        cout<<"month:" <<_month<< endl;
        cout<<"day:" <<_day<< endl<<endl ;
    }
    
    void Display () const
    {
        cout<<"Display () const" <<endl;
        cout<<"year:" <<_year<< endl;
        cout<<"month:" <<_month<< endl;
        cout<<"day:" <<_day<< endl<<endl;
    }

private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

void Test ()
{
    Date d1 ;
    d1.Display();  // 调用非const版本的Display()

    const Date d2;
    d2.Display();  // 调用const版本的Display()
}
1. const 对象可以调用非 const 成员函数吗?不可以
  • const 对象只能调用 const 成员函数,不能调用非 const 成员函数。这是因为 const 对象承诺不改变它的状态,而非 const 成员函数可能会修改对象的成员变量,因此编译器会阻止这种调用。
2. const 对象可以调用 const 成员函数吗?可以
  • const 对象可以调用 const 成员函数。const 成员函数不会修改对象的状态,所以对非 const 对象来说,调用 const 成员函数是安全的。
3. const 成员函数内可以调用其它的非 const 成员函数吗?不可以
  • const 成员函数中,不能调用非 const 成员函数。这是因为 const 成员函数承诺不改变对象的状态,但非 const 成员函数可能会修改对象的成员变量,因此编译器会阻止这种调用。
4. const 成员函数内可以调用其它的 const 成员函数吗?可以
  • const 成员函数可以调用 const 成员函数。因为 const 成员函数不会改变对象的状态,所以在非 const 成员函数中调用 const 成员函数是安全的。

进一步解释

  • const 限定符:当你在成员函数后面加上 const 限定符时,表示该函数不会修改对象的状态(即不会修改任何非 mutable 的成员变量)。

  • 编译器的行为:编译器会根据对象的 const 属性来确定哪些函数可以被调用。如果一个对象是 const 的,那么它只能调用不会改变其状态的函数。

1.10.2.取地址及const取地址操作符重载

class Date
{
public :
    Date* operator&()
    {
        return this ;
    }
    const Date* operator&()const
    {
        return this ;
    }
private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

1.10.3const 成员特点

  • 必须初始化const 成员变量必须在构造函数的初始化列表中初始化,不能在构造函数体内或其他地方赋值。
  • 不可修改:一旦初始化,const 成员变量的值不能再被修改。这确保了 const 成员在对象的生命周期内保持不变。

1.11explicit关键字

explicit 关键字在 C++ 中用于修饰构造函数和转换操作符,以防止隐式类型转换。以下是 explicit 关键字的主要作用和使用场景:

1. 防止隐式类型转换

在 C++ 中,如果构造函数接受一个参数(或多个参数),编译器可以自动将该类型转换为构造函数的参数类型。这种隐式转换可能会导致意外的行为,特别是在复杂的表达式和函数调用中。

例子:

class MyClass
{
public:
    MyClass(int x) : _x(x) {}
private:
    int _x;
};

void func(MyClass obj) {}

int main()
{
    func(10); // 隐式转换:10 被自动转换为 MyClass 对象
    return 0;
}

在上面的例子中,func(10) 会隐式地将整数 10 转换为 MyClass 对象,这可能不是你所期望的行为。

2. 使用 explicit 关键字

为了防止这种隐式转换,你可以使用 explicit 关键字修饰构造函数。这样,构造函数只能通过显式地进行类型转换来调用,而不能自动进行隐式转换。

修正后的例子:

class MyClass
{
public:
    explicit MyClass(int x) : _x(x) {}
private:
    int _x;
};

void func(MyClass obj) {}

int main()
{
    // func(10); // 编译错误:不能隐式地将整数转换为 MyClass 对象
    func(MyClass(10)); // 显式转换:需要显式地创建 MyClass 对象
    return 0;
}

3. 适用场景

  • 构造函数:当构造函数只有一个参数时,使用 explicit 关键字可以防止意外的隐式类型转换,确保类型转换只在明确的情况下进行。

  • 转换操作符:可以对转换操作符使用 explicit,以避免隐式类型转换。

示例:

class MyClass
{
public:
    explicit operator int() const { return _x; } // 显式转换为 int 类型
private:
    int _x;
};

int main()
{
    MyClass obj;
    int x = static_cast<int>(obj); // 显式转换
    return 0;
}

  • 隐式类型转换:编译器在某些情况下会自动进行类型转换,这可能会导致不期望的行为。
  • explicit 关键字:通过将构造函数或转换操作符标记为 explicit,你可以避免不必要的隐式类型转换,强制进行显式的类型转换,从而提高代码的安全性和可读性。

1.12 static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。

class A
{
public:
    A() {++_scount;}
    A(const A& t) {++_scount;}
    static int GetACount() { return _scount;}

private:
    static int _scount;
};

int A::_scount = 0;

void TestA()
{
    cout << A::GetACount() << endl; // 输出当前对象数量(应该是 0)
    A a1, a2;                      // 创建两个对象
    A a3(a1);                     // 使用拷贝构造函数创建一个新对象
    cout << A::GetACount() << endl; // 输出当前对象数量(应该是 3)
}

1.12.1static成员特性


        1. 静态成员为所有类对象所共享,不属于某个具体的实例
        2. 静态成员变量必须在类外定义,定义时不添加static关键字
        3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
        4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
        5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

     

1.12.2. 静态成员函数可以调用非静态成员函数吗? 不能

静态成员函数不能直接调用非静态成员函数。因为静态成员函数没有 this 指针,而非静态成员函数需要通过 this 指针来访问对象的成员(包括成员变量和其他成员函数)。由于静态成员函数不依赖于对象实例,它不能访问对象的非静态成员。

示例:

#include <iostream>

class MyClass {
public:
    static void StaticFunction() {
        // Error: cannot call non-static member function directly
        // NonStaticFunction();
    }

    void NonStaticFunction() {
        std::cout << "Non-static function called." << std::endl;
    }
};

int main() {
    MyClass::StaticFunction(); // 静态函数被调用
    return 0;
}

要在静态成员函数中调用非静态成员函数,你需要创建一个类的对象并通过这个对象调用非静态成员函数:

#include <iostream>

class MyClass {
public:
    static void StaticFunction() {
        MyClass obj;
        obj.NonStaticFunction(); // 通过对象调用非静态成员函数
    }

    void NonStaticFunction() {
        std::cout << "Non-static function called." << std::endl;
    }
};

int main() {
    MyClass::StaticFunction(); // 静态函数被调用
    return 0;
}

1.12.3. 非静态成员函数可以调用类的静态成员函数吗?可以

非静态成员函数可以调用静态成员函数,因为静态成员函数不依赖于特定对象的实例,它们属于类本身。因此,非静态成员函数可以直接调用静态成员函数,即使在静态成员函数没有 this 指针的情况下。

示例:

#include <iostream>

class MyClass {
public:
    static void StaticFunction() {
        std::cout << "Static function called." << std::endl;
    }

    void NonStaticFunction() {
        StaticFunction(); // 可以直接调用静态成员函数
    }
};

int main() {
    MyClass obj;
    obj.NonStaticFunction(); // 通过非静态成员函数调用静态成员函数
    return 0;
}

   2.C++11 的成员初始化

在 C++11 中,成员初始化得到了显著的增强。以下是一些主要的改进和新特性:

2.1. 成员初始化列表

C++11 引入了更简洁的成员初始化方式,可以直接在类定义中初始化成员变量。这使得成员变量的初始化更加直观和易于维护。

#include <iostream>

class MyClass
{
public:
    MyClass(int value = 0)
        : _value(value) // 通过初始化列表初始化
    {}

    void Print() const {
        std::cout << _value << std::endl;
    }

private:
    int _value;
};

int main() {
    MyClass obj(10);
    obj.Print(); // 输出: 10
    return 0;
}

2.2. 默认成员初始化

C++11 允许在类定义中直接给数据成员提供默认值。这使得即使构造函数没有显式初始化成员,成员也会有默认值。

#include <iostream>

class MyClass
{
public:
    MyClass(int value = 0)
    {
        // _value 和 _defaultValue 已有默认值
    }

    void Print() const {
        std::cout << _value << " " << _defaultValue << std::endl;
    }

private:
    int _value = 10; // 默认值为 10
    static const int _defaultValue = 20; // 静态常量成员
};

int main() {
    MyClass obj;
    obj.Print(); // 输出: 10 20
    return 0;
}

2.3. = default= delete

C++11 引入了 = default= delete 语法,用于明确控制构造函数、析构函数和其他特殊成员函数的默认行为。

  • = default:显式请求编译器生成默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数。
class MyClass
{
public:
    MyClass() = default; // 显式请求编译器生成默认构造函数
    MyClass(const MyClass&) = default; // 显式请求编译器生成拷贝构造函数
    MyClass& operator=(const MyClass&) = default; // 显式请求编译器生成拷贝赋值运算符
    ~MyClass() = default; // 显式请求编译器生成析构函数
};
  • = delete:显式禁止使用某些特殊成员函数,例如禁止拷贝构造函数或赋值运算符。
class MyClass
{
public:
    MyClass() = default;
    MyClass(const MyClass&) = delete; // 禁止拷贝构造函数
    MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值运算符
    ~MyClass() = default;
};

2.4. 初始化列表

C++11 支持使用初始化列表来初始化类成员,尤其适用于 std::initializer_list 类型的成员。

#include <iostream>
#include <initializer_list>
#include <vector>

class MyClass
{
public:
    MyClass(std::initializer_list<int> list)
        : _values(list) // 初始化 _values
    {}

    void Print() const {
        for (int value : _values) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }

private:
    std::vector<int> _values;
};

int main() {
    MyClass obj{1, 2, 3, 4, 5};
    obj.Print(); // 输出: 1 2 3 4 5
    return 0;
}
  1. 成员初始化列表:通过构造函数的初始化列表初始化成员变量。
  2. 默认成员初始化:直接在类定义中为数据成员提供默认值。
  3. = default= delete:显式控制特殊成员函数的行为。
  4. 初始化列表:使用 std::initializer_list 来初始化成员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值