什么是类?
在编程中,类是用来创建对象的模板。可以把类看作一个蓝图,它定义了对象的属性(特征)和方法(行为)。例如,如果我们有一个“学生”的类,它可能包含学生的名字、年龄等属性,以及学习、上课等方法。
类的基本结构
类的定义通常是这样的:
class ClassName {// 类体:由成员函数和成员变量组成
};
- class:这是定义类的关键字。
- ClassName:这是类的名字,通常以大写字母开头,以便与其他变量区分。
- {} :大括号内是类的主体,包含类的成员。
- ; :注意,类定义结束时后面必须有一个分号,这是语法要求,不能省略。
类的成员
类的主体中包含两种主要的成员:
-
成员变量(属性) 这些是类中定义的变量,用于存储对象的状态。例如,在“学生”类中,可以有
name
(名字)、age
(年龄)等属性。 -
成员函数(方法) 这些是类中定义的函数,用于描述对象可以执行的操作。例如,“学生”类可以有
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; // 成员变量
};
为了避免混淆,通常建议使用前缀或后缀来区分成员变量和参数。例如:
-
使用下划线前缀:
class Date { public:void Init(int year) {_year = year; // 明确区分} private:int _year; // 成员变量 };
-
使用小写字母 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:
protected
和private
修饰的成员在类外不能直接被访问。它们的作用是隐藏类的内部细节。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;
}
类的定义与实例化
类本身并不占用内存空间,它定义了对象的结构和行为。可以把类看作一个蓝图或模板,描述了对象应该包含哪些数据(成员变量)和可以执行哪些操作(成员函数)。
类的比喻
-
学生信息表:想象一个学生信息表,这个表格可以看作一个类,定义了学生的姓名、年龄、性别等属性。这个表格本身不占用数据,只是一个结构,实际的学生信息需要填写在这个表格中。
-
谜语的比喻:类可以被看作是一个谜语,而这个谜语的答案(谜底)就是一个具体的实例。比如,“年纪不大,胡子一把,主人来了,就喊妈妈”这个谜语的谜底是“山羊”。这里,“山羊”就是谜语的实例,而谜语本身则是描述“山羊”的类。
类的实例化
类的实例化是创建对象的过程。通过实例化,我们可以根据类的定义创建多个对象,每个对象都有自己的属性和状态。
实例化的过程
-
定义类:首先,我们定义一个类,比如
Person
类,描述一个人的属性和行为。class Person { public:int age; // 年龄void greet() {std::cout << "Hello!" << std::endl;} };
-
创建对象:然后,我们根据
Person
类创建一个或多个对象。int main() {Person person1; // 实例化一个对象person1.age = 25; // 设置属性person1.greet(); // 调用方法Person person2; // 再实例化一个对象person2.age = 30; // 设置不同的属性person2.greet(); // 调用方法return 0; }
物理空间的占用
在这个例子中,虽然Person
类本身并不占用内存,但person1
和person2
对象会占用实际的内存空间。每个对象都有自己的age
属性,存储了不同的值。
类与对象的比喻
类的实例化可以通过以下比喻来帮助理解:
-
建筑设计图:类就像是建筑设计图,描述了建筑的结构和组成部分。设计图本身并不占用空间,但根据设计图建造的房子(对象)才是实际存在的。每个房子都是根据同一设计图建造的,但每个房子都可以有不同的颜色、大小和装饰。
-
工厂与产品:可以将类视为工厂的蓝图,定义了生产特定类型产品的标准。工厂本身不生产任何产品,但它能根据蓝图生产出多个相同或不同的产品,每个产品都有自己的特性和状态。
类对象模型
计算类对象的大小
类的大小主要由其成员变量的大小决定,而不包括成员函数。计算机会将成员变量存储在对象中,而成员函数只会存在一份在代码段中。
结构体内存对齐规则
内存对齐是为了提高访问效率。规则如下:
- 第一个成员的地址偏移量为0。
- 其他成员变量要对齐到某个数字的整数倍地址。
- 结构体总大小为最大对齐数的整数倍。
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;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
默认构造函数的作用
很多人可能会质疑,编译器生成的默认构造函数有什么用。确实,当对象的成员是基本类型(如int
或char
)时,使用默认构造函数不会初始化这些成员,导致它们的值是随机的。
内置类型与自定义类型
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
在构造函数体执行之前就被赋予了初始值。
初始化列表使用须知
- 每个成员变量只能在初始化列表中出现一次(初始化只能初始化一次)。
- 必须在初始化列表中初始化的成员:
- 引用成员变量
const
成员变量- 自定义类型成员(且该类没有默认构造函数时)
- 优先使用初始化列表,尤其对于自定义类型的成员变量,初始化列表能够确保他们以最快的方式得到初始化,避免默认构造调用。
成员变量的初始化顺序
成员变量在类中声明的顺序决定了它们在初始化列表中的初始化顺序。无论在初始化列表中出现的顺序如何,实际的初始化顺序将按照成员声明的顺序进行。
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
析构函数
概念
析构函数是与构造函数相反的特殊成员函数。当对象的生命周期结束时,析构函数会被自动调用。析构函数的主要作用是清理对象使用的资源,例如动态分配的内存、打开的文件、网络连接等。
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性
析构函数具有以下特性:
- 命名规则:析构函数的名称是在类名前加上字符
~
(波浪号)。例如,class MyClass { ~MyClass(); };
。 - 无参数和无返回值:析构函数不接受参数,并且没有返回值类型。
- 唯一性:一个类只能有一个析构函数。如果未显式定义,编译器会自动生成一个默认的析构函数。
- 自动调用:当对象的生命周期结束时,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类。
拷贝构造函数的概念
拷贝构造函数是一个特殊的构造函数,用于通过已存在的对象创建一个新对象。它的主要作用是初始化新对象,使其与传入的对象具有相同的状态。
拷贝构造函数的特性
拷贝构造函数具有以下特性:
- 构造函数的重载形式:拷贝构造函数是构造函数的一个重载形式。
- 单个参数:它的参数只有一个,且必须是本类类型对象的引用,通常使用
const
修饰。这是因为如果使用值传递,会导致无限递归调用。 - 编译器生成的默认拷贝构造函数:如果未显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认的拷贝构造函数会按字节进行拷贝,这种拷贝称为浅拷贝。
示例代码
以下是一个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的析构函数都会被调用,可能导致双重释放内存
}
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
拷贝构造函数的使用场景
拷贝构造函数通常用在以下场景中:
- 使用已存在对象创建新对象:例如,
Date d2(d1);
。 - 作为函数参数:如果函数的参数是类类型对象,通常会使用拷贝构造函数。
- 作为函数返回值:返回类类型对象时,拷贝构造函数会被调用。
为了提高程序效率,通常在传递对象时使用引用类型,返回值时根据实际场景决定使用值返回还是引用返回。
运算符重载
C++引入运算符重载的目的是为了增强代码的可读性和可维护性。运算符重载允许程序员为自定义数据类型定义特定的运算符行为,其实质是在类中定义具有特殊名称的函数,这些函数的名称是由关键字 operator
加上需要重载的运算符符号构成的。运算符重载函数的返回值类型和参数列表与普通函数相似。
运算符重载的基本格式
函数原型如下:
返回值类型 operator 操作符(参数列表);
在运算符重载时,有几个重要的注意事项:
- 运算符重载不能通过连接其他符号来创建新的操作符,例如
operator@
是不允许的。 - 所有被重载的操作符必须至少有一个参数是类类型。
- 对于内置类型的运算符,例如整型
+
,其原有含义不能被改变。 - 当作为类成员函数进行重载时,运算符的参数数量往往比操作数少1,因为第一个参数是隐含的
this
指针。 - 有五个运算符是不能被重载的:
.*
、::
、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&
,返回引用以支持链式赋值。
赋值运算符重载时需要注意:
- 检测是否自我赋值。
- 返回
*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;
}
静态成员的特性
- 共享性:静态成员在所有类的对象之间共享,它们不属于某个特定的对象,而是存储在静态存储区。
- 类外定义:静态成员变量必须在类外进行定义,定义时不需要
static
关键字。 - 访问方式:静态成员可以通过
类名::静态成员
或者实例对象访问对象.静态成员
来访问。 - 无
this
指针:静态成员函数没有隐含的this
指针,因此它不能访问任何非静态成员。 - 访问权限:静态成员也受到类的访问控制(
public
、protected
、private
)的影响。
问题解答
-
静态成员函数可以调用非静态成员函数吗?
答案:不可以。静态成员函数不具有
this
指针,因此它无法访问类的非静态成员函数或非静态成员变量。如果尝试在静态成员函数中调用非静态成员函数,编译器将报错。class Example { public:static void staticFunction() {nonStaticFunction(); // 错误: 不能调用非静态成员}void nonStaticFunction() {std::cout << "Non-static function called." << std::endl;} };
-
非静态成员函数可以调用类的静态成员函数吗?
答案:可以。非静态成员函数可以自由地调用类的静态成员函数,因为非静态成员函数有
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;
}
友元函数的特点
- 友元函数可以访问类的私有和保护成员,但它不是类的成员函数。
- 友元函数不能用
const
修饰。 - 友元函数的声明可以在类定义的任意位置,且不受类的访问权限控制。
- 多个类可以共享同一个友元函数。
- 友元函数的调用方式与普通函数相同。
友元类
友元类的所有成员函数可以访问被声明为友元的类中的非公有成员。友元关系是单向的,意味着如果类 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;
}
友元类的特点
- 单向友元关系:如果类 B 是类 A 的友元,B 可以访问 A 的私有成员,但 A 不会自动访问 B 的私有成员。
- 友元关系不可传递:如果 B 是 A 的友元,C 是 B 的友元,则 C 并不是 A 的友元。
- 友元关系不能继承:友元关系不会随着类的继承而继承。
再次理解类与对象
在面向对象编程(OOP)中,类(Class)和对象(Object)是最基本的概念。正确理解这两个概念是编写有效和结构良好的程序的基础。
1. 描述现实世界的抽象
在现实生活中,我们遇到许多具体的实体,比如洗衣机、汽车、学生、员工等。计算机并不直接理解这些真实世界的实体,但可以通过抽象化的方式与这些实体建立联系。这个过程可以分为几个步骤:
-
抽象:将对象的关键信息和特征提取出来。对于洗衣机,我们可能会考虑其属性(如品牌、颜色、容量)和方法(如启动、停止、洗涤、脱水)。
-
定义类:使用编程语言(如 C++、Java、Python 等)将抽象的概念转变为类。类是一个蓝图,它描述了某种类型的对象的属性和行为,实际上是对这些对象的定义。
-
实例化对象:类只是一个概念,是一个模板;通过类,我们可以创建具体的对象。每个对象代表一个具体的实体,这些实体可以使用类中的定义的属性和方法。
-
模拟和操作对象:一旦对象被创建,我们可以通过编写代码来模拟现实生活中洗衣机的行为,例如让其启动或停止,获取其当前状态等。
2. 类与对象的关系
-
类:是定义对象的蓝图,包含了对象的属性和方法。它描述了对象的性质(数据)和功能(方法)。类本质上是一个自定义类型。
-
对象:是类的实例,是类中属性和方法的具体实现。每个对象都有自己独特的状态,但它们共享类定义的结构和行为。