史上最全关于C++类和对象详解

类和对象

在这里插入图片描述

前言

在C++中,面向对象有三大核心,分别是封装,继承和多态。

C++中一般用类和对象来进行封装。

类和对象是面向对象编程中的基础概念。类是一个抽象的概念,用于描述一类对象共有的属性和行为。而对象是类的实例,具有该类所描述的属性和行为。

类和对象是什么

类可以看作是一个模板,它定义了对象的属性和行为,并为我们提供了定义对象时所需要的具体信息。它包括数据成员和成员函数两部分。数据成员用于描述对象的属性,成员函数则用于描述对象的行为。

类相当于一个蓝图,它们规定了所有同类对象的共同特征,即这些对象具有相同的属性和相同的行为。而对象则是这个蓝图在现实世界中的具体实例。

举个例子,我们可以定义一个类叫做“人”,这个类有属性和行为,比如姓名、性别、年龄、说话、吃饭等。当我们需要描述一个具体的人时,我们就可以创建一个“人”的对象,例如“张三”就是一个“人”的对象。

类和对象是面向对象编程的基础,它们提供了一种结构化、模块化和抽象的方式来描述现实世界中的事物,是面向对象编程的关键所在。

案例

例如:

//在C++中,用class是一种定义类的关键字。使用class关键字定义一个类的语法如下:
class ClassName{     
    public:    //访问限定符
    int member1;
    char member2;
    float member3;
    void Print{
        cout<<"测试"<<endl;
     }
}

其中,ClassName是你所定义的类名称,public是访问限定符,在C++中,访问限定符一共有三个.

分别是:(public,private,protected).在这里,我们先不展开去详解,后面我们会特地去讲解.这里可以简单把它理解成一种访问变量的权限,就像是平时生活中的所谓的会员.

接着往下看,简而易见,就是定义变量和函数,跟平时C++中定义结构体的写法没啥区别,这里只是在场景搬到类里面.以上就是声明一个类的基本写法.

定义对象:

//这里定义了一个对象
ClassName p;
//在main函数中,我们使用(.)来调用对象的成员函数
//访问的对象成员.
//例如
p.Print();
p.member1();

this指针

在 C++ 中,this 是一个指向当前对象的指针。每个对象都有自己的 this 指针,它指向这个对象的地址。this 指针是在对象被创建时隐含地传递给成员函数的,可以用来访问对象的数据成员和成员函数。

this 指针的用法有如下几种:

1. 访问对象的数据成员

在 C++ 中,对象的数据成员和成员函数都是在对象内存空间中的。因此,在成员函数中可以通过 this 指针来访问对象的数据成员。例如:

class MyClass {
public:
    void func() {
        this->data_member = 10; // 使用 this 指针访问对象数据成员
    }
private:
    int data_member;
};

2. 访问对象的成员函数

在成员函数中,this 指针也可以用来访问对象的其它成员函数。例如:

class MyClass {
public:
    void func1() {
        this->func2(); //使用 this 指针访问成员函数
    }
    void func2() {
        // function body
    }
};

3. 返回对象本身

在成员函数中,可以使用 this 指针返回对象本身。这在实现链式调用等编程中很常用。

class MyClass {
public:
    MyClass& setX(int x) {
        this->x = x;
        return *this; // 返回对象本身,用于链式调用
    }
private:
    int x;
};

int main() {
    MyClass obj;
    obj.setX(10).setX(20).setX(30);
    return 0;
}

上述代码中,setX() 函数返回了对象本身,这样就可以链式调用 setX() 函数来设置对象的 x 值。

4. 解决参数名与成员名重名问题

当类中的成员变量和成员函数参数同名时,可以用 this 指针来解决冲突。例如:

class MyClass {
public:
    void func(int val) {
        this->val = val; // 使用 this 指针解决命名冲突
    }
private:
    int val;
};

上述代码中,data_member 和函数参数同名,使用 this 指针可以区分是访问成员变量还是函数参数。

5.总结

总之,this 指针是 C++ 中的一个非常重要的概念,它可以用来访问对象的数据成员和成员函数,解决命名冲突,实现链式调用等编程。

需要注意的是,在使用 this 指针时,一定要注意指针是否为空(nullptr),以避免程序发生未定义的行为。

类中的构造函数和析构函数

构造函数和析构函数是类中必不可少的函数.一般来说,构造函数用于对象初始化工作,析构函数用于清理回收分配给对象的空间.

构造函数

构造函数是一个与类同名的函数,它在对象被创建时自动执行,用于初始化对象的数据成员。构造函数可以重载,也就是说,可以定义多个构造函数,只要它们参数列表不同。

1. 默认构造函数

构造函数的声明和定义如下:

class ClassName {
public:
    // 默认构造函数
    ClassName();
};

默认构造函数没有参数,所以它的定义也很简单:

//我们可以直接在类里面定义函数
//但如果在类里面声明函数,在类外面定义,就需要
//在函数定义前添加类名和作用域解析符来指定该函数属于哪个类
ClassName::ClassName() {
    // 对象的数据成员默认初始化
}

int main()
{
    //如果构造函数无参数,我们可以直接这样定义一个该类的对象
    ClassName p;
    return 0;
}

2. 带参数的构造函数

如果需要带参数的构造函数,可以根据需要定义:

//1
ClassName::ClassName(Type param1, Type param2) {
    // 对象的数据成员初始化
    data_member1 = param1;
    data_member2 = param2;
    //...
}

//2
//这里是构造函数的特殊初始化,后面类中构造函数的初始化会讲
//带有默认参数的构造函数
//当定义对象不传参数时,不会报错,会将默认值作为参数
//ClassName::ClassName(Type param1=0, Type //param2=0) {
//    // 对象的数据成员初始化
//   data_member1 = param1;
//   data_member2 = param2;

//}

int main()
{
    //如果构造函数有参数且没有默认参数
    //定义对象的时候必须传参数,否则会报错
    ClassName p(参数1,参数2);
    
    //如果我们将上面的1注释,取消掉2的注释
    //那么,我们就可以直接如下定义对象
    ClassName p;
    return 0;
}

3. 拷贝函数

除了上述两种构造函数外,还有一种特殊的构造函数,也就是拷贝构造函数.

拷贝构造函数是 C++ 中的一个特殊的构造函数,它的作用是用一个现存的对象来初始化一个新的对象。通常情况下,拷贝构造函数用于以下几种情况:

1.对象的初始化

当对象被初始化时,拷贝构造函数会被调用。例如,下面的代码中的 Myclass obj2 = obj1;,会调用 obj1 的拷贝构造函数来初始化 obj2:

class MyClass {
public:
      int num;
      string num1;
       MyClass(){}; //无参的构造函数
      //拷贝构造函数
    MyClass(const MyClass& obj1) {
        // 对象的初始化
             this->num=obj1.num;
             this->num=obj1.num1;
    }
};
//如果想在定义对象的时候不传参数
//我们必须保证类里面有一个无参的构造函数
//当我们自己在类里面声明和定义构造函数时
//编译器就不会再默认生成一个无参的构造函数,需要我们自己声明和定义
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
2.函数传递参数

当对象作为参数传递给函数时,拷贝构造函数也会被调用。例如:

void foo(MyClass obj) {
    // function body
}
MyClass obj1;
foo(obj1); 
// 调用拷贝构造函数

在将 obj1 作为参数传递给 foo() 函数时,会调用一次拷贝构造函数。

3.赋值操作

当一个对象被赋值给另一个对象时,拷贝构造函数也会被调用。例如:

MyClass obj1;
MyClass obj2;
obj2 = obj1; // 调用拷贝构造函数

在 obj2 被赋值为 obj1 时,会调用一次拷贝构造函数。

4.总结

需要注意的是,拷贝构造函数并不总是被调用,它只在某些特定的情况下被调用。

此外,由于拷贝构造函数产生的副本通常需要分配新的内存空间,因此需要小心使用它,避免出现内存相关的问题。对于一些大型的对象,拷贝构造函数的调用也可能会导致程序的性能问题,因此需要进行优化。

最后,在默认情况下,如果没有为一个类定义构造函数,则编译器会自动生成一个默认构造函数。如果定义了带参数的构造函数,则需要定义一个默认构造函数,否则在类的实例化过程中可能会出现错误。

析构函数

析构函数是一个与类同名、前面加上 ~ 的函数,但它没有参数,且不能被重载,它在对象被销毁时自动执行,用于清理分配给对象的内存空间、释放其它资源等。

析构函数只有一个,没有参数,定义如下:

class ClassName {
public:
    // 构造函数
    ClassName();
    // 带参数的构造函数
    ClassName(Type param1, Type param2, ...);
    // 析构函数
    ~ClassName();
};

析构函数在对象被销毁时自动调用,其实现如下:

//因为是在类外面定义函数,这里我们需要添加
//作用域解析符::,以此来指定该函数属于哪个类
ClassName::~ClassName() {
    // 清理对象的资源    
}

需要注意的是,如果一个类有指针类型的数据成员,需要在析构函数中手动删除堆内存,否则会导致内存泄漏。

总结

总之,构造函数和析构函数是 C++ 类中两个重要的成员函数,两者都没有返回值,且无需声明返回值,构造函数可以有参数和重载,但析构函数不可以.

在类中,析构函数只有一个.构造函数用于初始化对象的数据成员,析构函数用于清理对象的资源。同时,它们也是 C++ 的面向对象编程中重要的概念,有助于提高代码的可维护性和可扩展性。

类中特殊的成员,函数和特殊的类

1.静态变量和函数:

C++ 中的静态变量和静态函数都属于类的静态成员,它们与类本身相关联,而不是与类的每个对象相关联。

静态变量可以被所有同一类的对象访问,而不需要创建类的对象。以下是关于类中静态变量和静态函数的更详细讲解:

1. 静态变量

静态变量是类的一个属性,与类的对象相对应。

它可以被所有同一类的对象共享,且只被初始化一次。通常来说,静态变量用于保存该类的所有实例的一些公共信息或状态,例如实例的个数等等。

静态变量是类的所有对象共享的成员变量,在内存中只有一份存储空间,不会随着对象的创建而分配新的空间。静态成员变量可以用类名和作用域解析符来访问,也可以用对象名和点运算符来访问。

语法上,静态变量需要在类的定义中声明,在类外部进行初始化。

class MyClass {
public:
    static int count; // 声明静态变量
};

//类中的静态变量必须在类外面初始化
int MyClass::count = 1; // 初始化静态变量

int main() {
    MyClass obj1, obj2;
    MyClass::count++; // 类名和作用域访问静态变量
    std::cout << obj1.count << obj2.count << endl; // 输出 2,对象名和点运算符来访问
    return 0;
}

2.静态成员函数

静态成员函数也属于类的静态成员,它们与类本身相关联,而不是与类的对象相关联。

与一般的成员函数不同,静态成员函数没有隐含的 this 指针,因为没有与之相关联的对象。

这意味着静态成员函数不能直接访问类的非静态成员,只可以访问类的静态成员。静态成员函数通常在对一组对象进行操作时,尤其是在需要在对象之间传递信息和状态时使用。

class MyClass {
public:
    static void printCount() { // 声明静态成员函数
        std::cout << count << std::endl; // 可以访问静态数据成员
    }
private:
    static int count; // 声明静态数据成员
};

int MyClass::count = 0; // 初始化静态数据成员

int main() {
    MyClass obj1, obj2;
    MyClass::printCount(); // 调用静态成员函数
    return 0;
}

3.总结

总之,静态成员和静态函数是类的静态成员,它们与类本身相关联而不是与类的对象相关联。

静态成员可以被所有同一类的对象共享,而静态函数不能访问类的非静态成员,只能访问类的静态成员。

需要注意的是,静态数据成员和静态成员函数一般用于某些公共信息和函数,应慎用,避免滥用,以免造成数据及程序控制流的混乱。

2.const修饰类的成员函数

在 C++ 中,const 关键字可以被用来修饰类的成员函数,即 const 成员函数。

被 const 关键字修饰的成员函数表示该函数不能修改类的任何成员变量,因此可以被const 类型的对象或非const类型的对象所调用。下面是关于 const 成员函数的一些详细讲解:

1.const 成员函数的声明

在成员函数的声明和定义中,const 关键字被用来修饰函数的参数列表后,表示该函数是一个 const 成员函数。以下是一个 const 成员函数的声明示例:

class MyClass {
public:
    int getCount() const
        // const 成员函数的声明
    // ...
};

在上述代码中,声明了一个返回值为 int 类型的 const 成员函数 getCount()。

2.const 成员函数的定义

在 const 成员函数的实现中,const 关键字需要放在函数体和参数列表之间,以表示该函数是 const 成员函数。以下是一个 const 成员函数的定义示例,其中的 const 关键字紧跟在参数列表后面:

int MyClass::getCount() const { // const 成员函数的定义
    return count; // const 成员函数可以访问类的数据成员,但不能修改它们的值
}

在上述代码中,定义了 const 成员函数 getCount() 的具体实现。需要注意的是,在 const 成员函数定义的后面,不能修改类的数据成员,只能访问它们的值。

3. 可以被 const 对象调用

被 const 关键字修饰的成员函数可以被 const 对象调用,而不会造成任何修改。

这在一些场景中非常有用,例如在定义常量对象时,我们可以声明一些 const 成员函数,以便在使用常量对象时保证数据的安全性和一致性。

class MyClass {
public:
    int getCount() const;
private:
    int count;
};

int MyClass::getCount() const {
    return count;
}

int main() {
    const MyClass obj; // 定义一个常量对象
    obj.getCount(); // 调用对象的 const 成员函数
    return 0;
}

在上述代码中,定义了一个 MyClass 类,并在其中声明了一个 const 成员函数 getCount()。在 main() 函数中,定义了一个常量对象 obj,并用它来调用 getCount() 函数。

4.总结

总之,const 成员函数在类中被常用以增加程序的可靠性和安全性。

const 成员函数可以被 const 对象调用,而且它不能修改任何成员变量,const 函数也可以被非常量对象调用,但反之不可以。

也就是说,非常量对象可以调用 const 成员函数,而 const 对象只能调用 const 成员函数。

这样做的原因是,const 成员函数不会修改类的状态,所以,当类的某些成员函数不需要修改类的成员变量时,可以使用 const 关键字来修饰该成员函数,以确保对象在调用这些函数时不会被修改。

3.友元函数和友元类

在 C++ 中,友元函数和友元类是一种特殊的机制,用于允许一个非成员函数或非本类的成员函数访问该类的私有成员。在下面的讲解中,我们将详细讲解友元函数和友元类的使用。

1. 友元函数

友元函数是一种定义在类外部且并不属于类的普通函数,但它可以访问该类的私有成员(private)和保护成员(protected)。

友元函数的声明需要在类的定义中进行,可以在公有、私有或保护成员函数之后添加 friend 关键字声明。示例代码如下:

class MyClass {
public:
   //这里是构造函数的特殊初始化,后面类中构造函数的初始化会讲
    MyClass(int num) : m_num(num) {}
    friend void printNum(const MyClass& obj); // 声明友元函数
private:
    int m_num;
};

void printNum(const MyClass& obj) { // 定义友元函数
    std::cout << obj.m_num << std::endl; // 可以访问私有成员 m_num
}

int main() {
    MyClass obj(42);
    printNum(obj); // 调用友元函数
    return 0;
}

在上面的示例中,定义了一个 MyClass 类,并在其中声明了一个友元函数 printNum。在 main() 函数中,调用了 printNum() 函数来输出 MyClass 类的私有成员 m_num 的值。

2.友元类

友元类指的是在一个类中声明了另一个类为其友元的情况。

被声明为友元类的类可以访问该类的所有私有成员和保护成员。

友元类的声明需要在类的定义中进行,可以在公有、私有或保护成员之后添加 friend 关键字声明。示例代码如下:

class MyClass {
public:
    MyClass(int num) : m_num(num) {}
    friend class MyFriendClass; // 声明友元类
private:
    int m_num;
};

class MyFriendClass {
public:
    void printNum(const MyClass& obj) { 
        // 定义一个可以访问 MyClass 私有成员的方法(函数)
        std::cout << obj.m_num << std::endl;
    }
};

int main() {
    MyClass obj(42);
    MyFriendClass obj2;
    obj2.printNum(obj); // 调用友元类中的方法
    return 0;
}

在上述示例中,MyClass 类中声明了 MyFriendClass 类为友元类。

MyFriendClass 类中定义了一个成员函数 printNum,在printNum中,可以访问 MyClass 类的私有成员 m_num。

在 main() 函数中,先定义了 MyClass 对象 obj,然后定义了 MyFriendClass 对象 obj2,使用 obj2调用 MyClass 对象的私有成员变量 m_num 的值。

3.总结

总之,友元函数和友元类是允许非成员函数或非本类的成员函数访问该类的私有成员的特殊机制。

友元函数和友元类的声明需要在类的定义中进行,可以在公有、私有或保护成员之后添加 friend 关键字声明。

友元函数和友元类可以访问该类的所有私有成员和保护成员,但应注意,友元类并不能直接访问将其视为友元类的私有成员变量,友元类只是表明在该类中,可以像在原类一样访问其成员和函数.

且使用时,应慎重考虑其安全性和可维护性。

访问限定符

在 C++ 中,有三种访问限定符,分别为 public、private 和 protected,它们是用于控制类成员访问权限的关键字。它们的作用是,通过修饰类中的成员变量和成员函数,控制这些成员是否可以被外部访问或继承类访问。

1. public 访问限定符

public 访问限定符作用于类的成员变量和成员函数,被 public 修饰的成员变量和成员函数可以被类的外部访问并且可以被继承类访问。以下示例演示了 public 访问限定符的应用:

class MyClass {
public:
    int publicNum; // 在 public 访问限定符的作用域内,publicNum 可以被外部访问
    void printPublicNum() { // 在 public 访问限定符的作用域内,printPublicNum 可以被外部访问
        std::cout << "publicNum = " << publicNum << std::endl;
    }
};

int main() {
    MyClass obj;
    obj.publicNum = 42; // publicNum 可以被外部访问并修改其值
    obj.printPublicNum(); // printPublicNum 可以被调用并打印其值
    return 0;
}

在上述代码中,定义了一个 MyClass 类,并在其中声明了一个 public 访问限定符。该类中的 public 成员变量 publicNum 和 public 成员函数 printPublicNum 都可以被外部访问和调用。

在 main() 函数中,使用对象 obj 访问并修改 publicNum 变量的值,并通过调用 printPublicNum() 方法打印该变量的值。

2. private 访问限定符

private 访问限定符同样作用于类的成员变量和成员函数,但被 private 修饰的成员变量和成员函数只能在类内部访问,不能公开给类的外部或继承类使用。以下示例演示了 private 访问限定符的应用:

class MyClass {
private:
    int privateNum; // 在 private 访问限定符的作用域内
    //privateNum 只能被当前类内部访问
    void printPrivateNum() { // 在 private 访问限定符的作用域内
        //printPrivateNum 只能被当前类内部访问
        std::cout << "privateNum = " << privateNum << std::endl;
    }
public:
    void setPrivateNum(int num) { // 在 public 访问限定符的作用域内
        //setPrivateNum 可以被外部调用,以修改 privateNum 的值
        privateNum = num;
    }
    void printPrivateNum_out() { // 在 public 访问限定符的作用域内
        //printPrivateNum_out 可以被外部调用
        printPrivateNum();
    }
};

int main() {
 MyClass obj;
 obj.setPrivateNum(42); // setPrivateNum 可以被外部调用,并修改 privateNum 的值
 obj.printPrivateNum_out(); // printPrivateNum_out 可以被外部调用,并打印 privateNum 的值
    return 0;
}

在上述代码中,定义了一个 MyClass 类,并在其中声明了一个 private 访问限定符和一个 public 访问限定符。private 成员变量 privateNum 和 private 成员函数 printPrivateNum 在类内部访问。

在 public 访问限定符的作用域内,public 成员函数 setPrivateNum 和 printPrivateNum_out 可以被外部调用以修改和打印 private 成员变量 privateNum 的值。

3. protected 访问限定符

(这里可以先跳过,因为涉及到了后面继承的相关知识)

protected 访问限定符只能用于继承类中,被 protected 修饰的成员变量和成员函数可以在当前类和其派生类中访问。以下示例演示了 protected 访问限定符的应用:

在 protected 访问限定符的作用域内定义了一个 age 成员变量,在继承类或子类中可以访问到该成员变量。

class Animal{
    protected:
    int age;
}

class Cat : public Animal {
public:
    void setAge(int a) { // Cat 类中的成员函数可以访问从父类 Animal 继承而来的 protected 成员变量 age
        age = a;
    }
    void printAge() { // Cat 类中的成员函数可以访问从父类 Animal 继承而来的 protected 成员变量 age
        std::cout << "Cat age = " << age << std::endl;
    }
};

int main() {
    Cat cat;
      // cat.age=3; error,protected类只能本类和继承类中访问
    cat.setAge(2); // setAge 是 Cat 类中的公有成员函数
    //可以访问从父类 Animal 继承而来的 protected 成员变量 age
    cat.printAge(); // printAge 是 Cat 类中的公有成员函数
    //可以访问从父类 Animal 继承而来的 protected 成员变量 age
    return 0;
}

在上述代码中,定义了一个 Animal 类,并在其中声明了一个 protected 成员变量 age。

再定义一个 Cat 类继承 Animal 类,其中包含两个公有成员函数 setAge() 和 printAge(),可以访问从父类 Animal 继承而来的 protected 成员变量 age。在 main() 函数中,为 Cat 对象 cat 的年龄赋值,然后通过打印年龄输出结果。

4.总结

总之,public、private 和 protected 是三种访问限定符,它们用于控制类成员访问权限。

其中,public 修饰的成员可以被本类和外部访问;private 修饰的成员只能在本类中访问;protected 修饰的成员可以在本类和继承类中访问。

构建程序时,我们需要根据实际需求,合理地使用访问限定符,以达到程序的正确性、高效性、安全性等方面的要求。

运算符重载

C++类中运算符的重载是指我们可以自己定义对应于一个类对象的各种运算符,如加法、减法、乘法、除法等,以适应不同的需求。

运算符重载使得我们能够用非常自然和直接的方式来操作类对象。在下面的讲解中,我们将详细讲解 C++ 类中运算符的重载。

1. 成员函数的运算符重载

成员函数运算符重载形式如下:

返回类型 operator运算符(形参列表) {
    // 运算符重载函数体
}

其中,operator是C++内置的运算符,形参列表可以为空或包含多个参数。示例代码如下:

class Vector {
public:
    int x, y;
    // 定义重载 + 运算符的成员函数
    Vector operator + (const Vector& vec) {
        Vector r;
        r.x = this->x + vec.x;
        r.y = this->y + vec.y;
        return r;
    }
};

int main() {
    Vector a = (1, 2), b = (3, 4);
    Vector c = a + b; // 调用重载 + 运算符的成员函数
    std::cout << c.x << "," << c.y << std::endl; // 输出运算结果
    return 0;
}

在上述代码中,我们定义了一个名为 Vector 的类,其中 x 和 y 是坐标值。

在该类中,我们通过重载+运算符的成员函数实现了两个 Vector 对象的相加操作。在 main() 函数中,声明了两个 Vector 对象 a 和 b,通过调用成员函数重载+运算符的方式实现两对象相加,得到结果交给新的 Vector 对象 c,最后输出计算结果。

2. 友元函数的运算符重载

友元函数运算符重载形式如下:

返回类型 operator运算符(形参列表) {
    // 运算符重载函数体
}

其中,operator是C++内置的运算符,形参列表可以为空或包含多个参数。示例代码如下:

class Vector {
public:
    int x, y;
    
    friend Vector operator + (const Vector& vec1, const Vector& vec2);
      void Print()
      {
          std::cout<<this->x<<" "<<this->y<<std::endl;
    }
};

Vector operator + (const Vector& vec1, const Vector& vec2)
{
    Vector sum;
        sum.x= vec1.x+vec2.x;
      sum.y=vec2.y+vec2.y;
}
int main()
{
    Vector a(2,3),b(4,5);
    Vector c=a+b;
    c.Print();
    return 0;
}

在上述代码中,我们定义了一个名为 Vector 的类,其中 x 和 y 是坐标值。和前面的例子不同的是,我们使用了友元函数的方式来实现运算符的重载。

在类定义中声明了一个全局函数 operator+ 作为友元函数,该函数有两个参数 vec1 和 vec2,并在函数体中实现了两个 Vector 对象的相加操作。在主函数中,声明了两个 Vector 对象 a 和 b,使用重载的+运算符通过调用友元函数实现两个 Vector 对象的相加,得到结果交给新的 Vector 对象 c,最后输出计算结果。

3.总结

友元函数和成员函数在C++类的运算符重载中并没有本质区别.

但是,对于访问类的私有或保护数据需要在外部函数中进行运算的情况,如果只使用成员函数,那么我们无法访问到该类对象的私有或保护数据。

这时我们就需要通过友元函数来实现对类对象私有或保护成员的访问,从而实现运算符重载。如果不使用友元函数,那么访问类中的私有变量或保护变量需要实现类的封装性会出现问题,是不被允许的。

因此,当运算符需要访问类的私有或保护数据时,我们需要定义一个友元函数来实现运算符的重载,并在该函数中声明该类为友元类。示例如下:

假设我们有一个名为 Complex 的复数类,其中包含了实部 real 和虚部 imaginary,它们是私有成员数据。我们想要定义一个运算符 + 重载函数,使得它可以将两个复数相加并返回结果。

由于运算符重载需要访问类的私有成员,因此需要定义为类的友元函数,而不能定义为类的成员函数。下面是一个可能的实现:

class Complex {
private:
    double real;
    double imaginary;
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imaginary(i) {}
    friend Complex operator+(const Complex &a, const Complex &b);
};

Complex operator+(const Complex &a, const Complex &b) {
    return Complex(a.real + b.real, a.imaginary + b.imaginary);
}

在上面的代码中,我们声明了一个友元函数 operator+(),它接受两个 Complex 类型的参数,并返回它们的和。由于这个函数被声明为 Complex 类的友元函数,所以它可以直接访问 Complex 类的私有成员 realimaginary,以计算出两个复数的和。

这个例子表明了在重载运算符时,如果要能够访问类的私有成员数据,需要将运算符重载定义为友元函数,而不能直接定义为类的成员函数。

但在 C++ 中,有一些运算符是不能被重载的,也就是不允许我们在程序中自定义它们的行为。不能重载的运算符包括:

  1. 作用域解析运算符 ::
  2. 条件运算符 ? :
  3. sizeof 运算符
  4. 成员选择运算符 .->
  5. 类型转换运算符 typeid
  6. 成员指针运算符 ’ . ’ 和 ’ * ’
  7. 编译预处理符号 ’ # ’

其中,前大部分运算符都比较容易理解,无法自定义它们的行为是因为它们在语言中扮演了很重要的角色。

类型转换运算符 typeid 用于检查一个表达式的类型,而不是将值转换为特定类型。这种运算符的行为与类的成员函数类似,也是基于表达式本身的类型,而不是运算符的操作数。

需要注意的是,在 C++11 中,可以通过使用关键字 decltype 来获取一个表达式的类型,从而实现有一定程度上的类型推断。因此,虽然不能重载 typeid 运算符,但可以使用 decltype 关键字来处理表达式的类型信息。

除了上述不能重载的运算符之外,其他的运算符均可以在程序中定义和重载。

类中构造函数的初始化

写到这里才想起还没填上文中关于构造函数初始化的坑,特地开一个专题讨论一下.

在 C++ 中定义类时,可以使用多种方式对类的数据成员进行初始化。常见的类中构造函数初始化的方式包括:

1. 无默认值初始化

在类中定义数据成员时,可以不指定默认初始值,在构造函数中通过参数进行初始化。例如:

class Person {
public:
    Person(string name, int age);
private:
    string name;
    int age;
};

Person::Person(string name, int age) {
    this->name = name;
    this->age = age;
}

上面的代码中,Person 类定义了两个私有成员变量 nameage,并在构造函数中进行了初始化。

2. 成员初始化列表初始化

在构造函数的参数列表后面使用 :,然后在后面跟上一系列名字 ( 表达式 ) 对的成员初始化列表,用于直接初始化一个对象,而不需要先创建对象然后再赋值。例如:

class Person {
public:
    Person(string name, int age) : name(name), age(age) {}
private:
    string name;
    int age;
};

注意,成员初始化列表初始化的顺序取决于成员在类中定义的顺序,而不是初始化列表中的顺序。

3. 使用默认参数的构造函数

可以为构造函数的参数设置默认参数,从而可以在对象创建时不需要传递这些参数。例如:

class Person {
public:
    Person(string name = "unknown", int age = 0) : name(name), age(age) {}
private:
    string name;
    int age;
};

上面的代码中,构造函数的参数 nameage 都设置了默认参数,因此创建 Person 对象时可以不传递任何参数。

  1. 委托构造函数

在一个构造函数中,可以调用同一个类中的另一个构造函数进行初始化,这个构造函数被称为委托构造函数。例如:

class Person {
public:
    Person() : Person("unknown", 0) {} // 委托构造函数
    Person(string name, int age) : name(name), age(age) {}
private:
    string name;
    int age;
};

在上面的代码中,第一个构造函数调用了同一个类中的另一个构造函数,这就是一个委托构造函数。

4.总结

这些是常见的类中构造函数初始化方式,每种方式都有其适用的情况。在实现类时,可以根据具体情况进行选择。
在这里插入图片描述

动态开辟以及动态开辟对象

1.前言

本来打算这就结束了,但仔细想一想,还有动态开辟没讲,为了方便后面讲动态开辟,顺便继续讲一讲动态开辟以及动态开辟对象.

2.动态开辟

动态开辟也叫动态内存分配,动态内存分配可以在程序运行时请求操作系统分配一段内存空间,用于存储对象或数据。在 C++ 中,通常使用 newdelete 运算符来进行动态内存分配。它们有点类似于C语言中的 ‘malloc’ 和 ‘free’ ,(当然,C++中也可以使用这些函数)下面给出一个经典的动态开辟的案例——动态开辟数组。

动态开辟数组指的是在程序运行时,根据需要动态地创建一个指定大小的数组。由于数组大小是在运行时决定的,因此需要使用动态内存分配来分配足够的内存空间,以存储新数组。下面是一个例子:

int main() {
    int size;
    cout << "Enter the size of the array: ";
    cin >> size;

    // 动态开辟数组
    int *arr = new int[size];
	   // 动态开辟int变量
	   int *p = new int;

    // 初始化数组并输出
    for (int i = 0; i < size; i++) {
        arr[i] = i;
        cout << arr[i] << " ";
    }
    cout << endl;

    // 释放内存
    delete[] arr;
	   delete p;
}

在上面的代码中,程序首先从用户获取一个整数 size,即新数组的大小。然后,在使用 new 操作符创建一个指定大小的数组,并将返回的指针存储在 arr 变量中。接下来,程序使用 for 循环对新数组进行初始化,并输出数组中的每个元素。最后,使用 delete[] 操作符释放数组占用的内存空间。

注意,与使用 ‘new’ 创建变量并直接用 ‘delete’ 释放内存不同,在使用 new 创建数组时,必须使用 delete[] 操作符来释放占用的内存,而不是使用 delete 操作符。delete[] 操作符会按照动态分配的顺序,依次释放数组占用的内存空间。

动态开辟数组是常见的动态内存分配用法之一,用于在运行时分配足够的内存空间,以存储需要的元素和数据。在实现动态开辟数组时,需要注意内存分配和释放的过程,确保程序不会因为动态内存问题而产生错误。

3.动态开辟对象

动态内存分配是在程序运行时请求使用操作系统分配的一种内存分配方式,主要通过使用关键字 newdelete 来实现。动态开辟对象即是通过 new 操作符在堆上分配内存空间来创建对象。下面详细讲解一下动态开辟以及动态开辟对象会发生的事情:

1.执行过程

1. 分配内存空间

在代码中使用 new 操作符创建对象时,程序会在运行时向操作系统请求开辟一段内存空间,来存储这个对象所需的空间。操作系统为程序返回了一段连续的内存地址,应该是堆内存的某个位置。

2. 调用对象的构造函数

在返回内存地址后,程序会自动调用对象的构造函数,来完成对对象的初始化工作。构造函数负责完成对象中成员变量的初始化,以及可能的后续操作,确保对象已经处于有效的状态。在此过程中,如果对象有指向其他内存的指针成员,需要为这些指针成员分配合适的内存空间,并确保它们指向正确的位置。

需要注意的是,如果类具有带有参数的构造函数和默认构造函数,则可以使用 new 运算符来创建一个对象而不传递任何参数时,系统会调用默认构造函数。如果类只有带有参数的构造函数,则不能使用无参的 new 运算符来创建对象。

3. 返回指向新对象的指针(this)

在对象创建并完成初始化后,构造函数会返回一个指向新对象的指针,该指针指向堆内存中的这个对象。

构造函数返回整个对象所在的内存空间的指针 (this), 这是一个隐式的返回语句,不必在构造函数中显式地指定返回值。因此,构造函数本身没有显式的返回值,但它会隐式返回一个包含整个对象的内存地址的指针。

程序将该指针存储在变量中,以便在需要时可以使用它来访问对象。

4. 访问并使用对象

对于动态开辟对象,程序通过指向该对象的指针来访问和使用它。程序可以使用 -> 运算符来访问对象的成员变量和成员函数,也可以通过指针运算符 * 来访问指针所指向的对象本身。

5. 释放内存空间

在使用完动态开辟对象后,程序需要显式地释放该对象所占用的内存空间。使用 delete 运算符可以将对象所占用的内存空间归还给操作系统,并销毁对象所占用的资源。如果不释放动态开辟的内存空间,程序会一直占用这段内存空间,导致内存泄露。

2.动态开辟案例

以下是一个经典的动态开辟对象的例子及相应的注释,可以帮助d读者更好地理解动态分配对象的用法和原理:

1.动态开辟对象
#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	   // 构造函数,用于初始化对象
    Person(string name, int age):name(name),age(age){} 
   // 成员函数,打印 name 和 age
	void print() { 
	  cout << "Name: " << name<< ", Age: " << age<< endl;
	} 
private:
    string name;
    int age;
};

int main() {
	//new操作符分配内存空间并调用构造函数来创建对象
    Person *p = new Person("Jack", 28); 
    p->print(); // 调用成员函数来输出 name 和 age
    delete p; // 释放动态分配的内存空间
    return 0;
}

在上面的例子中,我们首先定义了一个表示人物的 Person 类。该类有一个字符串类型的名字成员变量 name 和一个整型类型的年龄成员变量 age,以及一个带参数的构造函数来对这些成员变量进行初始化。

在主函数中,我们使用 new 操作符在堆上动态分配了一个 Person 类对象,并将返回的指针赋给了 p。然后,我们通过调用对象的成员函数 print() 来输出对象的名字和年龄信息。最后,我们使用 delete 操作符释放了动态分配的内存,这样可以防止内存泄漏。

2.动态开辟对象数组

以下是一个动态开辟对象数组的示例以及需要注意的事项:

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	  // 默认构造函数
    Person() : name_(""), age_(0) {} 
	// 带参数的构造函数
    Person(string name, int age) : name_(name), age_(age) {} 
    void print() {
		cout << "Name: " << name_ << ", Age: " << age_ << endl; 
	} // 成员函数,输出 name 和 age
private:
    string name_;
    int age_;
};

int main() {
    int n = 3; // 数组长度
	
	// 动态分配 Person 类的 n 个对象并将指针赋给 pArr
	//堆区(必须有默认构造函数)
    Person *pArr = new Person[n]; 
	
  // 循环遍历所有对象并初始化它们
   for (int i = 0; i < n; i++) { 
        string name;
        int age;
        cout << "请输入第 " << i + 1 << " 个人的名字:";
        cin >> name;
        cout << "请输入第 " << i + 1 << " 个人的年龄:";
        cin >> age;
        pArr[i] = Person(name, age);
    }
	
    // 循环遍历并打印所有对象
    for (int i = 0; i < n; i++) {
        pArr[i].print();
    }

    delete[] pArr; // 释放动态分配的内存空间
	
	//栈区(可以没有默认构造函数)
	 Person P1arr[]={("张三",18),("战鹰",3)};
	
    return 0;
}

在这个例子中,我们定义了一个表示人物的 Person 类。该类有一个字符串类型的名字成员变量 name 和一个整型类型的年龄成员变量 age,以及一个默认构造函数和一个带参数的构造函数来对这些成员变量进行初始化。

在主函数中,我们先定义了一个变量 n 来存储数组的长度,然后使用 new 操作符分配了 n 个 Person 类对象的内存空间,并将返回的指针赋给了 pArr。接下来,我们使用循环来遍历所有对象,并通过键盘输入初始化每个对象的 name 和 age。最后,我们再次遍历对象并使用对象的成员函数 print() 来打印每个对象的 name 和 age。

3.注意事项

1.new和delete

在动态开辟对象数组时,我们需要记得释放申请的内存空间。当使用一个动态分配的对象数组时,我们应该使用 delete[] 来释放它。这是因为 new [] 操作符创建的对象数组是作为一个连续的内存块来分配的,因此,我们需要使用 delete[] 来释放整个内存块,以保证内存释放的正确性。如果使用单个 delete 来释放对象数组,则可能会造成内存泄漏或者其他严重问题。

2.对象数组容量

动态分配对象数组的长度应该小于堆的剩余容量,否则 new 操作符可能会返回 NULL,表示无法分配所需的内存空间。

3.初始化问题

动态分配对象数组时,如果使用了带有参数的构造函数,则应该在循环中对每个对象进行初始化赋值。

4.默认构造函数

动态开辟对象数组时,系统会自动调用类的默认构造函数来对数组中的每个对象进行初始化,对于动态创建的对象数组(即在堆区分配的内存),类必须要有默认构造函数 , 否则会导致编译错误。而在(函数内且非动态开辟)栈区创建对象数组,则没有这个要求。

5. 释放内存

动态分配的对象应该用指针来管理,并已经在程序的适当位置释放掉,以避免内存泄漏或内存过量占用的问题。

6.消耗

动态分配对象数组不应该过于频繁,因为每次分配和释放内存都是一种消耗,容易成为程序的瓶颈。

7.总结

综上所述,对于创建动态对象数组,构造函数必须满足提供元素的初始化方式,析构函数必须用于释放对象数组的内存空间。同时,如果类自己定义了构造函数,就应该提供默认构造函数以保证对象创建的正确性;并且在类创建的过程中,如果需要使用具有参数的构造函数,则在循环中必须对每个对象进行赋值,这样才能符合我们的预期。

4.总结

总的来说,在动态开辟对象的过程中,程序需要完成内存分配、构造函数调用、对象访问和内存释放等多个步骤。因此,动态开辟对象时需要格外小心,确保在程序中正确地使用动态内存。

结尾

在这里插入图片描述

写到这里,忍不住想感慨一下,终于要结束了! 花了10几天的时间,秉着复习和整理的知识的心情,开启了这趟未知之旅,中间有迷茫,也有想过放弃,但庆幸的是,还是坚持下来了。
最后,虽然该博客耗费我大量的时间和精力,但奈何我本人水平有限,若文章中有疏漏和瑕疵的地方,望能谅解,也欢迎读者指正。
当然,也希望各位读者可以给个三连,创作不易,你的支持是我继续创作的最大动力。
我的这篇博客,应该能得到换你一个收藏吧?

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值