万字重谈C++——类和对象篇

什么是类?

在编程中,类是用来创建对象的模板。可以把类看作一个蓝图,它定义了对象的属性(特征)和方法(行为)。例如,如果我们有一个“学生”的类,它可能包含学生的名字、年龄等属性,以及学习、上课等方法。

类的基本结构

类的定义通常是这样的:

class ClassName {// 类体:由成员函数和成员变量组成
};
  • class:这是定义类的关键字。
  • ClassName:这是类的名字,通常以大写字母开头,以便与其他变量区分。
  • {} :大括号内是类的主体,包含类的成员。
  • ; :注意,类定义结束时后面必须有一个分号,这是语法要求,不能省略。

类的成员

类的主体中包含两种主要的成员:

  1. 成员变量(属性) 这些是类中定义的变量,用于存储对象的状态。例如,在“学生”类中,可以有 name(名字)、age(年龄)等属性。

  2. 成员函数(方法) 这些是类中定义的函数,用于描述对象可以执行的操作。例如,“学生”类可以有 study()(学习)和 attendClass()(上课)等方法。

类的定义方式

定义类时,我们有两种常见的方式:

声明和定义全部放在类体中

class Student {
public:void study() {// 学习的实现}
private:int age; // 年龄
};
  • 在这种方式中,所有的成员函数和成员变量都在类的定义内部。这种方式简单易懂,但编译器可能会将成员函数当成内联函数处理。

类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中

// 在 Student.h 文件中
class Student {
public:void study();
private:int age;
};// 在 Student.cpp 文件中
void Student::study() {// 学习的实现
}
  • 这种方式是更常见的做法,特别是在大型项目中。它有助于代码的组织和管理。
  • 当在 .cpp 文件中定义成员函数时,函数名前需要加上类名和作用域运算符 ::

成员变量命名规则的建议

在类中定义成员变量时,命名规则非常重要,尤其是为了区分成员变量与函数参数。以下是一些建议:

class Date {
public:void Init(int year) {// 这里的 year 可能会引起混淆year = year; // 这会导致问题,因为它将参数 year 赋值给自己}
private:int year; // 成员变量
};

为了避免混淆,通常建议使用前缀或后缀来区分成员变量和参数。例如:

  1. 使用下划线前缀:

    class Date {
    public:void Init(int year) {_year = year; // 明确区分}
    private:int _year; // 成员变量
    };
  2. 使用小写字母 m 作为前缀:

    class Date {
    public:void Init(int year) {mYear = year; // 明确区分}
    private:int mYear; // 成员变量
    };

这些命名规则有助于提高代码的可读性,减少错误的可能性。具体的命名约定可能会因公司或团队的要求而有所不同,但通常都建议使用某种前缀或后缀来明确区分成员变量和其他变量。

访问限定符

在C++中,类的访问限定符用于控制类成员(属性和方法)在类外的可见性和访问权限。通过使用访问限定符,我们可以实现封装的特性,让对象的内部状态和实现细节不被外部直接访问。

访问限定符的类型

public

public修饰的成员可以在类外直接被访问。这意味着任何地方的代码都可以使用这些成员。

class Dog {
public:void bark() {std::cout << "Woof!" << std::endl;}
};

protected 和 private

  • protectedprivate修饰的成员在类外不能直接被访问。它们的作用是隐藏类的内部细节。
  • protected成员可以在派生类中访问,而private成员只能在定义它的类内部访问。
class Dog {
private:int age; // 只能在Dog类内部访问protected:void wagTail() { // 可以在Dog类和其派生类中访问std::cout << "Wagging tail!" << std::endl;}
};

作用域

  • 访问权限的作用域从访问限定符出现的位置开始,到下一个访问限定符出现为止。如果没有后续的访问限定符,作用域直到类的结束。

默认访问权限

  • 在C++中,如果没有显式指定访问权限,class的默认访问权限为private,而struct的默认访问权限为public。这是因为struct需要兼容C语言的特性。

【面试题】 问题:C++中struct和class的区别是什么?

解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。

封装

封装是面向对象编程的一个重要特性,它将数据(属性)和操作数据的方法(行为)结合在一起,隐藏内部实现细节,仅公开必要的接口供外部使用。

封装的例子

想象一下计算机的使用,用户只需通过开关机键、键盘和鼠标与计算机进行交互,而不需要了解内部的硬件如何工作。计算机厂商通过外壳隐藏了复杂的内部结构,只提供简单的操作接口。

在C++中,封装通过类实现。我们可以将数据和操作数据的方法结合在一起,通过访问权限控制哪些方法可以被外部访问。

类的作用域

类定义了一个新的作用域,类的所有成员都在这个作用域内。当我们在类体外定义成员函数时,需要使用作用域操作符 :: 来指明成员属于哪个类。例如:

class Person {
public:void PrintPersonInfo();
};void Person::PrintPersonInfo() {std::cout << "Person info" << std::endl;
}

类的定义与实例化

类本身并不占用内存空间,它定义了对象的结构和行为。可以把类看作一个蓝图或模板,描述了对象应该包含哪些数据(成员变量)和可以执行哪些操作(成员函数)。

类的比喻

  • 学生信息表:想象一个学生信息表,这个表格可以看作一个类,定义了学生的姓名、年龄、性别等属性。这个表格本身不占用数据,只是一个结构,实际的学生信息需要填写在这个表格中。

  • 谜语的比喻:类可以被看作是一个谜语,而这个谜语的答案(谜底)就是一个具体的实例。比如,“年纪不大,胡子一把,主人来了,就喊妈妈”这个谜语的谜底是“山羊”。这里,“山羊”就是谜语的实例,而谜语本身则是描述“山羊”的类。

类的实例化

类的实例化是创建对象的过程。通过实例化,我们可以根据类的定义创建多个对象,每个对象都有自己的属性和状态。

实例化的过程

  1. 定义类:首先,我们定义一个类,比如Person类,描述一个人的属性和行为。

    class Person {
    public:int age; // 年龄void greet() {std::cout << "Hello!" << std::endl;}
    };
  2. 创建对象:然后,我们根据Person类创建一个或多个对象。

    int main() {Person person1; // 实例化一个对象person1.age = 25; // 设置属性person1.greet(); // 调用方法Person person2; // 再实例化一个对象person2.age = 30; // 设置不同的属性person2.greet(); // 调用方法return 0;
    }

物理空间的占用

在这个例子中,虽然Person类本身并不占用内存,但person1person2对象会占用实际的内存空间。每个对象都有自己的age属性,存储了不同的值。

类与对象的比喻

类的实例化可以通过以下比喻来帮助理解:

  • 建筑设计图:类就像是建筑设计图,描述了建筑的结构和组成部分。设计图本身并不占用空间,但根据设计图建造的房子(对象)才是实际存在的。每个房子都是根据同一设计图建造的,但每个房子都可以有不同的颜色、大小和装饰。

  • 工厂与产品:可以将类视为工厂的蓝图,定义了生产特定类型产品的标准。工厂本身不生产任何产品,但它能根据蓝图生产出多个相同或不同的产品,每个产品都有自己的特性和状态。

类对象模型

计算类对象的大小

类的大小主要由其成员变量的大小决定,而不包括成员函数。计算机会将成员变量存储在对象中,而成员函数只会存在一份在代码段中。

结构体内存对齐规则

内存对齐是为了提高访问效率。规则如下:

  1. 第一个成员的地址偏移量为0。
  2. 其他成员变量要对齐到某个数字的整数倍地址。
  3. 结构体总大小为最大对齐数的整数倍。

this指针

this指针是C++中一个隐含的指针,指向当前对象。当成员函数被调用时,this指针自动传递给函数,指向调用该函数的对象。

this指针的特性

this指针的类型

  • this指针的类型是类类型* const,这意味着它是一个指向当前对象的指针,并且在成员函数内部不能改变this指针的指向。换句话说,你不能让this指针指向其他对象。

只能在成员函数内部使用

  • this指针是在成员函数中隐式存在的。你不能在类的外部或静态成员函数中使用this指针,因为它仅与特定的对象实例相关联。

this指针的本质

  • this指针实际上是成员函数的第一个隐含参数。当对象调用成员函数时,编译器会将对象的地址作为实参传递给this指针。因此,类的对象并不在自身中存储this指针。

this指针的传递

  • 在大多数情况下,this指针是由编译器通过特定寄存器(如x86架构下的ecx寄存器)自动传递的,用户无需显式传递。

面试题

1. this指针存在哪里?

this指针存储在栈中。当一个对象调用成员函数时,创建一个新的栈帧,this指针会作为该栈帧的一部分存在。每次调用成员函数时,this指针的值会被设置为调用该函数的对象的地址。

2. this指针可以为空吗?

在正常情况下,this指针不应该为空。this指针指向当前对象的地址,如果在成员函数中使用了空指针调用该函数,程序会崩溃,通常会导致访问违规。但在某些情况下,例如在类的静态成员函数中,this指针是不可用的,因为静态成员函数不依赖于任何特定的对象实例。

默认构造函数的生成

在C++中,如果一个类没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数。这个默认构造函数的主要作用是初始化对象的成员变量。

重要特性

  • 自动生成:如果用户没有定义任何构造函数,编译器会生成一个无参构造函数。
  • 显式定义的影响:一旦用户显式定义了构造函数(无论是无参的还是带参的),编译器将不再生成默认构造函数。

示例代码分析

以下是一个Date类的示例,展示了无参构造函数和带参构造函数的使用:

#include <iostream>
using namespace std;class Date {
public:// 无参构造函数Date() {_year = 1900; // 默认值_month = 1;_day = 1;}// 带参构造函数Date(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};void TestDate() {Date d1; // 调用无参构造函数d1.Print(); // 输出: 1900-1-1Date d2(2015, 1, 1); // 调用带参构造函数d2.Print(); // 输出: 2015-1-1// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明Date d3(); // 这不是创建对象,而是声明了一个返回类型为Date的函数
}int main() {TestDate();return 0;
}

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

默认构造函数的作用

很多人可能会质疑,编译器生成的默认构造函数有什么用。确实,当对象的成员是基本类型(如intchar)时,使用默认构造函数不会初始化这些成员,导致它们的值是随机的。

内置类型与自定义类型

C++将类型分为内置类型(基本类型)和自定义类型(用户定义的类型)。内置类型在没有显式初始化的情况下,其值是未定义的,而自定义类型的成员会调用其默认构造函数进行初始化。

class Time {
public:Time() {_hour = 0;_minute = 0;_second = 0;cout << "Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date {
private:// 基本类型(内置类型)int _year; // 未初始化,值是随机的int _month; // 未初始化,值是随机的int _day; // 未初始化,值是随机的// 自定义类型Time _t; // 调用Time的默认构造函数
};int main() {Date d; // 创建Date对象,_t会被初始化return 0;
}

C++11的改进

在C++11中,可以在类的声明中为内置类型的成员变量提供默认值。这确保了即使使用默认构造函数,内置类型的成员变量也会被初始化。

class Date {
private:// 基本类型(内置类型)并提供默认值int _year = 1970; // 初始化为1970int _month = 1;   // 初始化为1int _day = 1;     // 初始化为1Time _t; // 自定义类型,调用Time的默认构造函数
};int main() {Date d; // 创建Date对象,所有成员都被初始化return 0;
}

编译时的注意事项

在使用构造函数时,用户需要注意以下几点:

  • 如果定义了带参数的构造函数,程序将无法使用默认构造函数,除非显式定义一个。
  • 在声明对象时,使用“无参数构造函数”时,后面不要加括号,否则会被解释为函数声明。

构造函数体赋值

在 C++ 中,构造函数用于在创建对象时为其成员变量提供合适的初始值。不过需要注意的是,构造函数体内的赋值语句并不被称为成员变量的初始化。只能将 构造函数体内的赋值 称为给成员变量 赋初值。初始化是一个特定的过程,只能执行一次,而赋值可能在构造过程中执行多次。

初始化列表

初始化列表是 C++ 构造函数中用来初始化成员变量的一种方式。它提供了一种语法,使得成员变量可以在构造函数体执行前就被初始化,从而避免不必要的默认构造和然后再赋值的开销。

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

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

如上所示,在初始化列表中,成员变量 _year_month 和 _day 在构造函数体执行之前就被赋予了初始值。

初始化列表使用须知

  1. 每个成员变量只能在初始化列表中出现一次(初始化只能初始化一次)。
  2. 必须在初始化列表中初始化的成员:
    • 引用成员变量
    • const 成员变量
    • 自定义类型成员(且该类没有默认构造函数时)
  3. 优先使用初始化列表,尤其对于自定义类型的成员变量,初始化列表能够确保他们以最快的方式得到初始化,避免默认构造调用。

成员变量的初始化顺序

成员变量在类中声明的顺序决定了它们在初始化列表中的初始化顺序。无论在初始化列表中出现的顺序如何,实际的初始化顺序将按照成员声明的顺序进行。

class A {
public:A(int a): _a1(a), _a2(_a1) {}  // _a2 会使用 _a1初始化void Print() {std::cout << _a1 << " " << _a2 << std::endl;}
private:int _a1;int _a2;
};

示例代码分析

int main() {A aa(1);aa.Print();  // 输出结果分析
}

对于上述代码,构造 A 的对象时,_a1 初始化为 1。接下来 _a2 将会使用 _a1 的值初始化。由于在此时 _a1 已经是 1,所以 _a2 被赋值为 1

因此,输出将是:

1 1

析构函数

概念

析构函数是与构造函数相反的特殊成员函数。当对象的生命周期结束时,析构函数会被自动调用。析构函数的主要作用是清理对象使用的资源,例如动态分配的内存、打开的文件、网络连接等。

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特性

析构函数具有以下特性:

  1. 命名规则析构函数的名称是在类名前加上字符~(波浪号)。例如,class MyClass { ~MyClass(); };
  2. 无参数和无返回值:析构函数不接受参数,并且没有返回值类型。
  3. 唯一性:一个类只能有一个析构函数。如果未显式定义,编译器会自动生成一个默认的析构函数。
  4. 自动调用:当对象的生命周期结束时,C++编译器会自动调用析构函数。

示例代码

以下是一个使用析构函数的示例,展示了如何在类中管理动态分配的内存:

#include <iostream>
#include <cstdlib> // malloc, free
using namespace std;typedef int DataType;class Stack {
public:Stack(size_t capacity = 3) {_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array) {perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data) {if (_size < _capacity) {_array[_size] = data;_size++;} else {cout << "Stack is full!" << endl;}}// 析构函数~Stack() {if (_array) {free(_array);_array = NULL;_capacity = 0;_size = 0;cout << "Stack memory freed!" << endl;}}private:DataType* _array;int _capacity;int _size;
};void TestStack() {Stack s;s.Push(1);s.Push(2);
}int main() {TestStack(); // 当TestStack结束时,Stack对象s被销毁,析构函数被调用return 0;
}

在这个示例中,Stack类包含一个动态分配的数组_array。在析构函数中,使用free释放了分配的内存,确保不会发生内存泄漏。

编译器生成的析构函数

如果一个类中包含自定义类型的成员变量,编译器生成的默认析构函数会自动调用这些自定义类型成员的析构函数。以下是一个示例:

class Time {
public:~Time() {cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date {
private:int _year = 1970; // 内置类型int _month = 1;   // 内置类型int _day = 1;     // 内置类型Time _t;          // 自定义类型
};int main() {Date d; // 创建Date对象return 0; // 当d的生命周期结束时,调用Date的析构函数,进而调用Time的析构函数
}

输出分析

Date对象d的生命周期结束时,编译器会自动调用Date的析构函数(如果没有显式定义,则使用默认析构函数)。在这个过程中,Time类的析构函数也会被调用,尽管在main函数中没有直接创建Time对象。

重要注意事项

  • 析构函数的调用:创建哪个类的对象,销毁时调用的就是该类的析构函数。即使在类中没有显式定义析构函数,编译器也会自动生成一个,以确保所有成员(特别是自定义类型)都能正确释放资源。
  • 内置类型的处理:对于内置类型的成员变量,析构函数不需要进行特殊处理,因为它们在对象销毁时会自动释放内存。
  • 无法重载:一个类只能有一个析构函数,且无法重载。
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

拷贝构造函数的概念

拷贝构造函数是一个特殊的构造函数,用于通过已存在的对象创建一个新对象。它的主要作用是初始化新对象,使其与传入的对象具有相同的状态。

拷贝构造函数的特性

拷贝构造函数具有以下特性:

  1. 构造函数的重载形式:拷贝构造函数是构造函数的一个重载形式。
  2. 单个参数:它的参数只有一个,且必须是本类类型对象的引用,通常使用const修饰。这是因为如果使用值传递,会导致无限递归调用。
  3. 编译器生成的默认拷贝构造函数:如果未显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认的拷贝构造函数会按字节进行拷贝,这种拷贝称为浅拷贝。

示例代码

以下是一个Date类和Time类的示例,展示了如何使用拷贝构造函数:

#include <iostream>
using namespace std;class Time {
public:Time() {_hour = 1;_minute = 1;_second = 1;}// 拷贝构造函数Time(const Time& t) {_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}private:int _hour;int _minute;int _second;
};class Date {
public:Date(int year = 1970, 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;_t = d._t; // 调用Time的拷贝构造函数}private:int _year;int _month;int _day;Time _t; // 自定义类型
};int main() {Date d1; // 创建d1对象Date d2(d1); // 使用拷贝构造函数创建d2对象return 0;
}

在这个示例中,Date类的拷贝构造函数会在创建d2时被调用,而Time类的拷贝构造函数也会在Date类的拷贝构造函数中被调用。

浅拷贝与深拷贝

如果类中包含指针或动态分配的内存,编译器生成的默认拷贝构造函数会执行浅拷贝。浅拷贝会导致多个对象指向同一块内存,这可能会导致资源管理的问题,比如双重释放内存。

示例:浅拷贝导致的问题

#include <iostream>
#include <cstdlib> // malloc, free
using namespace std;class Stack {
public:Stack(size_t capacity = 10) {_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array) {perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;}~Stack() {if (_array) {free(_array);_array = nullptr;}}// 拷贝构造函数(未定义,使用默认的浅拷贝)// Stack(const Stack& s) = default; // 如果显式定义为default,编译器会自动生成private:DataType* _array;size_t _size;size_t _capacity;
};int main() {Stack s1; // 创建s1对象Stack s2(s1); // 使用拷贝构造函数创建s2对象(浅拷贝)return 0; // 当程序结束时,s1和s2的析构函数都会被调用,可能导致双重释放内存
}

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

拷贝构造函数的使用场景

拷贝构造函数通常用在以下场景中:

  1. 使用已存在对象创建新对象:例如,Date d2(d1);
  2. 作为函数参数:如果函数的参数是类类型对象,通常会使用拷贝构造函数。
  3. 作为函数返回值:返回类类型对象时,拷贝构造函数会被调用。

为了提高程序效率,通常在传递对象时使用引用类型,返回值时根据实际场景决定使用值返回还是引用返回。

运算符重载

C++引入运算符重载的目的是为了增强代码的可读性和可维护性。运算符重载允许程序员为自定义数据类型定义特定的运算符行为,其实质是在类中定义具有特殊名称的函数,这些函数的名称是由关键字 operator 加上需要重载的运算符符号构成的。运算符重载函数的返回值类型和参数列表与普通函数相似。

运算符重载的基本格式

函数原型如下:

返回值类型 operator 操作符(参数列表);

在运算符重载时,有几个重要的注意事项:

  1. 运算符重载不能通过连接其他符号来创建新的操作符,例如 operator@ 是不允许的。
  2. 所有被重载的操作符必须至少有一个参数是类类型。
  3. 对于内置类型的运算符,例如整型 +,其原有含义不能被改变。
  4. 当作为类成员函数进行重载时,运算符的参数数量往往比操作数少1,因为第一个参数是隐含的 this 指针。
  5. 有五个运算符是不能被重载的:.*::sizeof?: 和 .

示例:全局的等于运算符重载

以下为 Date 类的示例,演示全局运算符重载 ==

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}friend bool operator==(const Date& d1, const Date& d2) {return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}private:int _year;int _month;int _day;
};void Test() {Date d1(2018, 9, 26);Date d2(2018, 9, 27);std::cout << (d1 == d2) << std::endl;  // 输出 0 (false)
}

在这个示例中,注意到全局运算符重载使得成员变量必须是公有的,因此可能影响类的封装性。可以通过使用友元函数或将运算符重载定义为成员函数来解决这个问题。

示例:成员函数的等于运算符重载

下面是将运算符重载改为成员函数的示例:

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}bool operator==(const Date& d2) const {return _year == d2._year && _month == d2._month && _day == d2._day;}private:int _year;int _month;int _day;
};

在这个示例中,operator== 被定义为 Date 类的成员函数,其中使用 const 关键字修饰,表明该函数不会修改类的任何成员。

赋值运算符重载

赋值运算符重载的格式如下:

  • 参数类型const T&,通过引用传递以提高效率。
  • 返回值类型T&,返回引用以支持链式赋值。

赋值运算符重载时需要注意:

  1. 检测是否自我赋值。
  2. 返回 *this 以支撑链式赋值操作。

示例代码如下:

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date& operator=(const Date& d) {if(this != &d) {  // 防止自我赋值_year = d._year;_month = d._month;_day = d._day;}return *this;  // 返回自身引用}private:int _year;int _month;int _day;
};

赋值运算符只能作为类的成员函数重载,不能定义为全局函数。这是因为编译器会自动生成一个默认的赋值运算符,如果用户再在类外定义一个全局的重载,就会与编译器生成的函数产生冲突。

自定义运算符重载示例

在 Date 类中继续添加其他运算符的重载,例如自增操作符和日期算术运算符:

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date& operator++() {  // 前置++_day += 1;return *this;}Date operator++(int) {  // 后置++Date temp = *this;  // 保存当前日期_day += 1;return temp;  // 返回变更之前的日期}// 其他运算符重载...private:int _year;int _month;int _day;
};

在上述代码中,前置和后置自增操作符的重载遵循了相应的规则和约定,我们保证了高效的操作和正确的行为。

日期类的实现

在 main 函数中,我们可以看到日期类的使用:

int main() {Date d1(2022, 1, 13);Date d = d1++;  // d: 2022, 1, 13; d1: 2022, 1, 14d = ++d1;      // d: 2022, 1, 15; d1: 2022, 1, 15return 0;
}

这个示例展示了如何利用重载的运算符来进行日期对象的日常操作。

const成员

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

1. const 对象可以调用非 const 成员函数吗?

答案:不可以。对于常量对象(const 对象),编译器确保其状态不能被修改,因此如果尝试调用非 const 成员函数会导致编译错误。这是因为非 const 成员函数可能改变对象的状态,而 const 对象的状态是不允许被修改的。例如:

const Date d(2022, 1, 13);
d.Print(); // 可以
d.SomeNonConstFunction(); // 错误:const 对象不能调用非 const 成员函数

2. 非 const 对象可以调用 const 成员函数吗?

答案:可以。非 const 对象可以调用 const 成员函数。const 成员函数承诺不修改对象的状态,因此可以安全地在可变对象上调用它们。例如:

Date d(2022, 1, 13);
d.Print(); // 可以,调用 const 成员函数

3. const 成员函数内可以调用其他非 const 成员函数吗?

答案:不可以。const 成员函数不能调用非 const 成员函数,因为这可能会导致状态改变,违背了 const 成员函数的目标。例如:

class Date {
public:void Modify() { _year++; } // 非 const 成员函数void Print() const {Modify(); // 错误:不能在 const 成员函数中调用非 const 成员函数}
private:int _year;
};

4. 非 const 成员函数内可以调用其他 const 成员函数吗?

答案:可以。非 const 成员函数可以调用 const 成员函数,因为 const 成员函数不修改对象状态,调用是安全的。例如:

class Date {
public:void Print() const {std::cout << "Year: " << _year << std::endl;}void Modify() {Print(); // 可以,因为 Print 是 const 成员函数}
private:int _year;
};

静态成员的概念

在 C++ 中,使用 static 关键字修饰的类成员被称为静态成员,这包括静态成员变量和静态成员函数。静态成员属于类本身,而不是某个特定的对象,因此它们在所有类的对象之间共享。

实现一个类,计算程序中创建的类对象数量

下面是一个简单的示例,通过静态成员变量来记录对象的数量:

#include <iostream>class ObjectCounter {
public:ObjectCounter() {count++; // 每创建一个对象,计数增加}~ObjectCounter() {count--; // 每销毁一个对象,计数减少}static int getObjectCount() {return count; // 静态成员函数可以访问静态成员变量}private:static int count; // 声明静态成员变量
};// 静态成员变量在类外进行定义初始化
int ObjectCounter::count = 0; // 初始化为0int main() {ObjectCounter obj1; // count = 1ObjectCounter obj2; // count = 2std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 输出 2{ObjectCounter obj3; // count = 3std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 输出 3} // obj3 被销毁,count = 2std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 输出 2return 0;
}

静态成员的特性

  1. 共享性:静态成员在所有类的对象之间共享,它们不属于某个特定的对象,而是存储在静态存储区。
  2. 类外定义:静态成员变量必须在类外进行定义,定义时不需要 static 关键字。
  3. 访问方式:静态成员可以通过 类名::静态成员 或者实例对象访问 对象.静态成员 来访问。
  4. 无 this 指针:静态成员函数没有隐含的 this 指针,因此它不能访问任何非静态成员。
  5. 访问权限:静态成员也受到类的访问控制(publicprotectedprivate)的影响。

问题解答

  1. 静态成员函数可以调用非静态成员函数吗?

     

    答案:不可以。静态成员函数不具有 this 指针,因此它无法访问类的非静态成员函数或非静态成员变量。如果尝试在静态成员函数中调用非静态成员函数,编译器将报错。

    class Example {
    public:static void staticFunction() {nonStaticFunction(); // 错误: 不能调用非静态成员}void nonStaticFunction() {std::cout << "Non-static function called." << std::endl;}
    };
  2. 非静态成员函数可以调用类的静态成员函数吗?

     

    答案:可以。非静态成员函数可以自由地调用类的静态成员函数,因为非静态成员函数有 this 指针,可以访问类的所有成员,包括静态成员。

    class Example {
    public:static void staticFunction() {std::cout << "Static function called." << std::endl;}void nonStaticFunction() {staticFunction(); // 可以调用静态成员函数}
    };

友元

友元关系在 C++ 中提供了一种突破类封装的机制,可以让特定的函数或类访问类的私有成员。虽然友元可以方便地访问私有数据,增加了程序的灵活性,但过多使用会导致高耦合,从而损害封装性。友元可以分为两种类型:友元函数和友元类。

友元函数

友元函数是定义在类外的普通函数,但为了让其能够访问类的私有和保护成员,需要在类内使用 friend 关键字声明。

重载 operator<<

在实现重载 operator<< 时,由于 cout 是流对象,我们无法将 operator<< 定义为成员函数,因为成员函数的第一个参数始终是 this 指针。而 << 操作符需要一个流对象作为其第一个参数。因此,我们必须将其定义为全局函数,并使用友元函数来访问期望的类成员。

#include <iostream>
using namespace std;class Date {friend ostream& operator<<(ostream& _cout, const Date& d);friend istream& operator>>(istream& _cin, Date& d);public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}private:int _year;int _month;int _day;
};// 定义友元函数
ostream& operator<<(ostream& _cout, const Date& d) {_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}istream& operator>>(istream& _cin, Date& d) {_cin >> d._year >> d._month >> d._day;return _cin;
}int main() {Date d; cin >> d; // 输入示例: 2023 12 25cout << d << endl; // 输出: 2023-12-25return 0;
}

友元函数的特点

  1. 友元函数可以访问类的私有和保护成员,但它不是类的成员函数。
  2. 友元函数不能用 const 修饰。
  3. 友元函数的声明可以在类定义的任意位置,且不受类的访问权限控制。
  4. 多个类可以共享同一个友元函数。
  5. 友元函数的调用方式与普通函数相同。

友元类

友元类的所有成员函数可以访问被声明为友元的类中的非公有成员。友元关系是单向的,意味着如果类 A 是类 B 的友元,B 的成员可以访问 A 的私有成员,但反之不成立

友元关系示例
class Time {friend class Date; // 声明 Date 为 Time 的友元类public:Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second) {}private:int _hour;int _minute;int _second;
};class Date {
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}void SetTimeOfDate(int hour, int minute, int second) {// 直接访问 Time 类的私有成员_t._hour = hour;_t._minute = minute;_t._second = second;}private:int _year;int _month;int _day;Time _t;  // Time 类型的成员变量
};int main() {Date d(2023, 12, 25);d.SetTimeOfDate(10, 30, 45); // 设置时间return 0;
}
友元类的特点
  1. 单向友元关系:如果类 B 是类 A 的友元,B 可以访问 A 的私有成员,但 A 不会自动访问 B 的私有成员。
  2. 友元关系不可传递:如果 B 是 A 的友元,C 是 B 的友元,则 C 并不是 A 的友元。
  3. 友元关系不能继承:友元关系不会随着类的继承而继承。

再次理解类与对象

在面向对象编程(OOP)中,类(Class)和对象(Object)是最基本的概念。正确理解这两个概念是编写有效和结构良好的程序的基础。

1. 描述现实世界的抽象

在现实生活中,我们遇到许多具体的实体,比如洗衣机、汽车、学生、员工等。计算机并不直接理解这些真实世界的实体,但可以通过抽象化的方式与这些实体建立联系。这个过程可以分为几个步骤:

  1. 抽象:将对象的关键信息和特征提取出来。对于洗衣机,我们可能会考虑其属性(如品牌、颜色、容量)和方法(如启动、停止、洗涤、脱水)。

  2. 定义类:使用编程语言(如 C++、Java、Python 等)将抽象的概念转变为类。类是一个蓝图,它描述了某种类型的对象的属性和行为,实际上是对这些对象的定义。

  3. 实例化对象:类只是一个概念,是一个模板;通过类,我们可以创建具体的对象。每个对象代表一个具体的实体,这些实体可以使用类中的定义的属性和方法。

  4. 模拟和操作对象:一旦对象被创建,我们可以通过编写代码来模拟现实生活中洗衣机的行为,例如让其启动或停止,获取其当前状态等。

2. 类与对象的关系

  • :是定义对象的蓝图,包含了对象的属性和方法。它描述了对象的性质(数据)和功能(方法)。类本质上是一个自定义类型。

  • 对象:是类的实例,是类中属性和方法的具体实现。每个对象都有自己独特的状态,但它们共享类定义的结构和行为。

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/75401.shtml

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    18认识Qt坐标系

    平面直角坐标系(笛卡尔坐标系) 数学上的坐标系 右手坐标系 计算机中的坐标系 左手坐标系 坐标系的原点(0,0) 就是屏幕的左上角 /窗口的左上角 给 Qt 的某个控件,设置位置,就需要指定坐标.对于这个控件来说, 坐标系原点就是相对于父窗口/控件的. QPushButton 的父元素/父控件/父…

    量子计算与人工智能的结合:未来科技的双重革命

    引言 在过去几十年里&#xff0c;人工智能&#xff08;AI&#xff09;和计算能力的提升一直是推动科技进步的重要力量。然而&#xff0c;随着深度学习和大规模数据处理的发展&#xff0c;传统计算架构的算力瓶颈逐渐显现&#xff0c;人工智能的训练和推理效率受到了限制。在此背…

    SEO长尾词优化策略精要

    内容概要 长尾关键词优化是SEO策略中实现精准流量捕获的核心环节。本文从定位方法、搜索意图分析、词库构建三个维度切入&#xff0c;系统阐述如何通过数据化工具筛选高转化潜力词&#xff0c;并结合用户行为路径优化内容架构。具体而言&#xff0c;内容将覆盖关键词挖掘工具的…

    基于大模型的主动脉瓣病变预测及治疗方案研究报告

    目录 一、引言 1.1 研究背景 1.2 研究目的 1.3 研究意义 二、大模型预测主动脉瓣病变原理 2.1 大模型介绍 2.2 数据收集与处理 2.3 模型训练与优化 三、术前预测与评估 3.1 主动脉瓣病变类型及程度预测 3.2 患者整体状况评估 3.3 手术风险预测 四、术中应用与监测…

    进程和内存管理

    目录 一.进程的基本信息 1.1进程的定义 1.2进程的特征 1.3进程的组成 1.4线程产生的背景 1.5线程的定义 1.6进程与线程的区别 1.7进程的类别 1.8进程的优先级 1.8.1进程优先级的概念 1.8.2PRI和NI 1.9僵尸进程 1.9.1僵尸进程的定义 1.9.2僵尸进程产生的原因 1.9…

    css动态设置div宽高,calc函数

    在css中使用calc函数 calc() 是 CSS 中的一种函数&#xff0c;用于动态计算长度值。它允许你在 CSS 属性中进行数学运算&#xff0c;结合不同的单位&#xff08;如 px、%、em 等&#xff09;&#xff0c;从而创建更加灵活和响应式的布局 表达式规则 运算符&#xff1a;支持加…

    飞浆PaddlePaddle 猫狗数据大战

    猫狗数据大战 1 数据集的准备以及处理操作1.1 数据集1.2 文件解压操作&#xff08;python&#xff09; 1.3 数据的分类1.4 创建训练集和测试集 2 网络构建CNN版本--DeepID 人脸识别网络结构DeepID 与 CNN 网络结构的差异 3 深度学习模型训练和推理的核心设置4 制图5 训练6 预测…

    Spring Boot后端开发全攻略:核心概念与实战指南

    &#x1f9d1; 博主简介&#xff1a;CSDN博客专家、全栈领域优质创作者、高级开发工程师、高级信息系统项目管理师、系统架构师&#xff0c;数学与应用数学专业&#xff0c;10年以上多种混合语言开发经验&#xff0c;从事DICOM医学影像开发领域多年&#xff0c;熟悉DICOM协议及…

    PPT助手:一款集计时、远程控制与多屏切换于一身的PPT辅助工具

    PPT助手&#xff1a;一款集计时、远程控制与多屏切换于一身的PPT辅助工具 &#x1f4dd;&#x1f3a4; 在现代化的演讲和演示中&#xff0c;如何高效地控制PPT进程、保证展示的流畅性与精准性&#xff0c;成为了每个演讲者必须面对的挑战。无论是商务汇报、学术演讲&#xff0…

    WEB安全--文件上传漏洞--php伪协议的利用

    一、伪协议介绍 1.1、内容 在 PHP 中&#xff0c;伪协议通常指的是一种通过特定的 URL 协议方案实现某些特殊功能或行为的方式。伪协议通常并不是标准的协议&#xff08;如 HTTP、HTTPS&#xff09;&#xff0c;而是由应用程序或开发者自定义的“伪”协议&#xff0c;用于执行…

    高级:高并发架构面试题深度解析

    一、引言 在现代互联网应用开发中&#xff0c;高并发架构设计是确保系统在高负载下仍能稳定、高效运行的关键。面试官通过相关问题&#xff0c;考察候选人对高并发系统设计的理解、架构模式的掌握以及在实际项目中解决问题的能力。本文将深入剖析高并发系统的设计原则、常见的…

    Opencv之dilib库:表情识别

    一、简介 在计算机视觉领域&#xff0c;表情识别是一个既有趣又具有挑战性的任务。它在人机交互、情感分析、安防监控等众多领域都有着广泛的应用前景。本文将详细介绍如何使用 Python 中的 OpenCV 库和 Dlib 库来实现一个简单的实时表情识别系统。 二、实现原理 表情识别系统…

    【动态规划】线性dp——LIS和LCS

    参考文章 子序列 一个序列 A &#xff1d; a 1 , a 2 , … , a n A&#xff1d;a_1,a_2,…,a_n A&#xff1d;a1​,a2​,…,an​ 中任意删除若干项&#xff0c;剩余的序列叫做 A 的一个子序列。也可以认为是从序列 A 按原顺序保留任意若干项得到的序列。&#xff08;例如&…

    umi框架开发移动端h5

    1、官网&#xff1a;https://umijs.org/ 2、创建出来的项目 yarn create umi yarn start3、推荐目录结构 . ├── config │ └── config.ts ├── public//静态资源 ├── dist ├── mock │ └── app.ts&#xff5c;tsx ├── src │ ├── .umi │ ├── .um…

    《Golang高性能网络编程:构建低延迟服务器应用》

    在本文中&#xff0c;我们将深入探讨Golang高性能网络编程&#xff0c;帮助您构建低延迟服务器应用。我们将介绍Golang的网络编程特性、优化技巧和实际案例&#xff0c;让您更好地理解和应用Golang在网络编程领域的优势。 高性能网络编程简介 什么是Golang高性能网络编程 高性能…

    循环结构- P1217-回文质数-第三十四天

    洛谷题单 第三十四天&#xff1a;4.3&#xff08;周四&#xff09; 题目&#xff1a;循环结构–P1217 注意&#xff01;&#xff01;&#xff01;本题的解法在初学阶段足矣&#xff0c;使用埃氏筛即可全部AC&#xff08;高级算法&#xff0c;优化时间复杂度&#xff09;&…

    github镜像网站的使用

    很多时候我们无法访问github 那么我们可以网上搜索镜像网站 比如 https://blog.csdn.net/eytha/article/details/144797222 这里可以找到一些镜像站 然后直接编辑 c:/user/xxx/.gitconfig 内容如 [user]name xxxxemail xxxxhotmail.com [gui]recentrepo D:/ProjectFolder/t…

    论定制开发开源 AI 智能名片 S2B2C 商城小程序源码在零售变革中的角色与价值

    摘要&#xff1a;本文深入探讨了新零售中 O2O 模式的特点与局限性&#xff0c;指出其虽有导流作用但难以成为企业转型适应消费大环境的主力做法。强调解决零售根本问题需依靠大零售概念&#xff0c;包括业态融合、情境创造、分解渗透等。同时引入定制开发开源 AI 智能名片 S2B2…

    硬件工程师零基础入门教程(三)

    27.二极管的基本结构 二极管的结构就是一个PN节&#xff0c;导通后肯定会存在压降&#xff08;硅管≈0.7V&#xff1b;锗管≈0.3V&#xff09;。 其结构就像一个漏斗结构&#xff0c;普通二极管只能单向导通。 注意&#xff1a;二极管两端不能直接接大于二极管导通压降的电压…

    ollama导入huggingface下载的大模型并量化

    1. 导入GGUF 类型的模型 1.1 先在huggingface 下载需要ollama部署的大模型 1.2 编写modelfile 在ollama 里面输入 ollama show --modelfile <你有的模型名称> eg: ollama show --modelfile qwen2.5:latest修改其中的from 路径为自己的模型下载路径 FROM /Users/lzx/A…