对象(Object)、类(Class)
简单来说,类是图纸,对象是根据图纸造出来的具体东西。
类 (Class):一个模板或蓝图。它描述了某一种事物 **应该有什么样的属性(特征)**和 能做什么样的事情(行为)。比如,我们定义一个“猫”类。
对象 (Object):一个实例 (Instance)。它是根据“类”这个图纸创造出来的实实在在的个体。比如,一只叫“大橘”的橘猫。
一. 数据抽象概述
(下面的概念会考问答题。)
WARNING**面向对象程序设计(object-oriented programming)**是把程序构造成由若干对象组成,每个对象由一些数据以及对这些数据所能实施的操作所构成的封装体。
对象的特征(数据、操作以及对外接口)由相应的类来描述,一个类所描述的对象特征可以从其他的类继承。
对数据的操作通过向包含数据的对象发送消息(调用对象类对外接口中的操作)来实现。
**对象(object)**是数据及对数据的操作构成的封装体。
**类(class)**描述了对象的基本特征(包含哪些数据与操作)
**继承(inheritance)**是指在定义一个类时,可以利用已有类的一些特征描述,即先把已有类的一些特征描述包含进来,再定义新的特征。主要涉及两个类,父类和子类,C++中叫基类和派生类。
**多态性(polymorphism)**是程序设计中的一个重要概念。多态性的一般含义是:某一论域中的某个元素存在多种形式和解释。
**绑定(binding)**或联编、定联,是指确定对多态元素的某个使用是多态元素的哪一种形式。绑定可分为静态绑定与动态绑定。静态绑定(static binding)也称前期绑定(early binding),是指在编译时刻确定对多态元素的使用;动态绑定(dynamic binding)也称后期绑定或延迟绑定(late binding),是指在程序运行时来确定对多
态元素的使用。
二. 类
一个示例:
class Date{ public: void set(int y, int m, int d); // 访问控制 // 成员函数 bool is_leap_year(); // 成员函数 void print(); // 成员函数 ......private: int year,month,day; // 访问控制 // 数据成员};1. 数据成员
数据成员的类型可以是任意的C++类型(除了void)。但是在声明一个数据类型的时候,如果一个数据成员的类的定义还没有定义完,那只能用这个类型的指针或者引用类型。
NOTE为什么呢?因为数据成员的本质恰恰是需要在对象中拥有一块属于自己的、大小确定的内存空间。
当定义一个类的对象的时候,比如
Student zhangsan;,计算机会做一件非常重要的事情:为这个对象分配内存。分配多大的内存呢?就是把它所有数据成员需要占用的内存加起来那么大。
void类型为什么不行?void是空的,0字节。void类型又不占任何空间。那到底该预留多大呢?0字节吗?一个不占任何空间的成员有什么意义呢?甚至无法往里面存任何东西。**为什么可以用一个没有定义过的、或定义不完整的类的指针?**因为在同一个编译环境下,所有(数据)指针的字节大小都是相同的,它大小可是确定的(比如在64位系统上是8字节)。
可以通过**前向声明(Forward Declaration)**告诉编译器有这么个类,尽管定义不完整,也可以声明指针。
// 前向声明 (Forward Declaration)class Student;// 现在,即使编译器还没看到 Student 的完整定义,它也可以处理 Student*// 因为它知道所有指针的大小都一样。class Teacher {private:Student* bestStudent; // OK! 声明一个指向 Student 的指针成员public:void setBestStudent(Student* s); // OK! 参数是 Student 指针Student* getBestStudent(); // OK! 返回值是 Student 指针};// 完整的类定义// 现在我们必须提供 Student 的完整定义了,因为后面要用到它的具体成员class Student {public:string name;void study() { /* ... */ }};同理,一个类中的数据成员也不能是自己——完整定义自己前,你都不知道自己有多大。这也循环定义了。
但是搞一个自己的指针还是可以的。这也很常用。比如链表
class Node {public:int data; // 节点存储的数据Node* next; // 指向下一个节点的指针};
类的定义中,数据成员属于这个类的对象。在创建对象前,这里说明的数据成员并不占据内存空间。所以一般不能给它赋初值。
class A{ int x=0; // Error const double y=0.0; // Error......};(课本第210页,或者6.2.1)。
(但是实际上我经常这么写,没啥问题,这是基于比较旧的标准(C++98)在C++11以及之后的所有新标准,都可以直接给非静态成员赋初值了,不过这里考试还是当不行吧。)
2. 成员函数
定义类的成员函数的具体函数体的时候,可以放在类定义中,也可以在定义外。
如果放在类定义里面,就是按照内联函数处理。
定义在其他地方的时候,为了和全局函数和其他的区别,要做返回值和函数名之间加上<类名>::
class Date{public: void set(int y, int m, int d); // 成员函数的声明 bool is_leap_year(); // 成员函数的声明 void print(); // 成员函数的声明private: int year,month,day;};void Date::set(int y, int m, int d) // 类外定义的成员函数{ year = y; month = m; day = d;}bool Date::is_leap_year() // 类外定义的成员函数{ return (year%4 == 0 && year%100 != 0) || (year%400==0);}void Date::print() // 类外定义的成员函数{ cout<<year<<"."<<month<<"."<<day;}类成员函数名也可以重载,和全局的重载是一样的。
3. private,public,protected
public:成员的访问不受限制,在程序中的任何地方都可以访问一个类的 public 成员。
private:成员只能在本类和友元中访问。
protected:成员只能在本类、友元和派生类中访问。(和private一样,多一个派生类)
C++中,类class的成员默认是private。结构struct和联合union的成员的默认是public。
C++ 的访问控制是基于“类”(Class-level),而不是基于“对象”(Object-level)。
这句话的意思是: 只要一个函数是 Complex 类的成员函数,那么它就可以访问任何 Complex 对象的私有成员,而不仅仅是调用它的那个对象的私有成员。
三. 对象
1. 创建对象
直接创建对象:直接定义变量。(如果是局部,就被放在栈里面,内存自动管理,结束后也不需要你手动析构,不会发生内存泄漏)(如果是全局对象,在静态存储区,或者说数据段中)。
class A { ...};
A a;间接创建对象:new一下。(放在堆中,手动管理内存。)
A * a = new A;// ...delete a; // 结束了要手动销毁2. 对象的操作
对象可以赋值。
A=B,可以把B的数据成员的值分别赋值给左边。当然也可以自己定义赋值。
对象可以是做返回值。
Date g1(const Date &d) { return d; // 创建一个临时对象做返回值,用d对其初始化。}
Date& g1(const Date &d) { return d; // 返回d,返回值为对象的引用。}是不是临时对象还是引用,取决于返回值的类型。
3. this指针
一个类A,可能有多个对象a1,a2,...,那执行一个类内函数的时候,怎么知道是对哪个类操作呢?,C++ 设计了一个巧妙的机制:每当成员函数被调用时,系统都会偷偷地、自动地传递一个额外的参数给这个函数,这个参数就是 this 指针。(类似python类中函数的第一个self)
大部分情况下,this可以省略,但是如果成员函数的参数名和成员变量名完全相同,就要用this
class Box {private: int width;public: void setWidth(int width) { // 如果你写 width = width; 编译器无法区分,这句代码没有意义 // 必须用 this 来指明左边的是“本对象的成员变量width” this->width = width; }};4. 构造与析构
构造函数
WARNING**构造函数(constructor)**是对象类中定义或声明的与类同名、无返回值类型的成员函数。创建对象的时候,系统会先给对象分配空间,然后用对象类的构造函数来初始化。
对构造函数的调用,属于对象创建的一部分,创建后就不能再调用了。以下是非法的。
a.A(); // Error不带参数的构造函数是默认构造函数(或者所有参数都有默认值的)。但是可以重载其他的构造函数。
类的构造函数一般是公开的,有时也会是私有的,作用是限制该类对象的范围,这时候只能在本类或友元中创建。
在创建动态的对象数组时,只能用默认构造函数进行初始化。
A a;a = A(10); // 创建了一个临时对象,并赋值给 a前面输了C++98不许直接在类定义里赋初值,那要是const的数据成员怎么办?可以用成员初始化表:
class A { int x; const int y; int &z;public: A(): z(x), y(1) { x = 0; }}A() : x(0), z(x), y(1) // 成员初始化表{
}在成员初始化表中,成员初始化的书写次序并不决定它们的初始化次序,数据成员的初始化次序由它们在类定义中的说明次序决定。例如,对于上面的类 A,其数据成员的初始化次序为:x(初始化为 0)、y(初始化为 1)和 z(初始化为引用 x)。
为什么在 C++ 中需要对这个次序做出规定?
我的理解是,首先,析构函数一般就一个。析构的顺序是恒定的,要按顺序的。有些成员之间有先后关系,先构造的,后析构。默认析构按照声明的顺序从后到前,那构造函数中顺序也就确定了,按照声明顺序从前到后。
如果一个类A的成员是一个对象B。那创建A的时候,也会调用B的构造函数来初始化这个成员。
默认是调用默认构造函数,如果要调用非默认构造函数,则需要在初始化表中显式指出。
析构函数
WARNING**析构函数(destructor)**是名为“
~<类名>”且没有参数和返回值类型的一个特殊的成员函数。当对象消亡时,在系统收回它所占的内存空间之前,会自动调用对象类的析构函数。
例如:
class A{ int x;public: A(); // 构造函数 ~A(); // 析构函数 ......};
......void f(){ A a; // 调用 a 的构造函数 A() ......} // 调用 a 的析构函数 ~A()需要注意的是,在撤销动态对象数组时,如果没写 delete 操作中的 “[]”,则只有数组中的第一个对象的析构函数会被调用。
然后类里面new的要自己去释放空间。
一个类A,如果数据成员有类B,C,怎么析构?假设是这样的
class A {private: B b_member; // b_member 在前 C c_member; // c_member 在后
public: A() { // ... A 构造函数的代码 ... } ~A() { // ... A 析构函数的代码 ... }};构造顺序是B->C,构造完后,才会调用A()构造出A.
析构的顺序反之。先执行~A()到结束,然后再调用~C(),~B()。
如果没有提供析构函数的类,如果它有成员对象,则编译程序会隐式地为之提供一个析构函数,该析构函数的作用是调用成员对象类的析构函数(但这并不是说直接写在这个函数里的,这里析构函数其实是空的{},作用就是直接跳过,到析构成员这一步)
5. 拷贝构造函数
就和默认构造、析构函数一样,拷贝构造函数,也是编译器会自动生成的一种函数。
WARNING在创建一个对象时,如果用另一个同类的对象对其进行初始化,将会调用一个特殊的构造函数——在创建一个对象时,如果用另一个同类的对象对其进行初始化,将会调用一个特殊的构造函数——拷贝构造函数。
class A {public: A(); A(const A& a); // 拷贝构造函数}要点:创建一个对象时、用另一个同类对象初始化,才会调用拷贝构造函数。
它的本质是一个构造函数,特殊之处在于它的参数是同一个类的另一个对象。
直接看A(const A& a);就能看出来了:
A( … ): 这说明它是一个构造函数。
A& a: 参数 a 是对源对象(被拷贝的对象)的一个引用 (&)。这样做是为了效率,避免在传递参数时又发生一次拷贝。
const: const 关键字告诉编译器,在拷贝过程中,我们保证不会修改源对象 a。这是一个好习惯,也更安全。
什么时候会调用拷贝构造函数?
情况 1,直接初始化。
A a1; // 调用默认构造函数,造出房子 a1A a2(a1); // 克隆 a1,造出 a2。明确调用拷贝构造函数。A a3 = a1; // **注意!** 这也是克隆,不是赋值!因为 a3 是在定义时初始化的。A a4 = A(a1); // 这也是拷贝构造函数情况 2:把对象作为值参数传递给函数。
void f(A x); // 函数 f 接受一个 A 类型的参数 x...A a; // 造一个对象 af(a); // 把 a 传给函数 ff(a) 被调用时,程序并不是把 a 本身送进函数。为了保护原始的 a 不被函数内部意外修改,C++ 会在函数 f 的内部创建一个临时副本 x,然后用 a 来初始化这个 x。这个过程,就是通过调用拷贝构造函数完成的。x 是 a 的一个克隆品。
情况 3:函数按值返回一个对象。
A f() { A a; ... return a; // 函数要返回一个 A 类型的对象}...A b = f(); // 用函数的返回值来初始化 b当函数 f 执行到 return a; 时,程序需要把 a 的值带出函数。它会创建一个临时的、匿名的对象,并用 a 来初始化这个临时对象(调用拷贝构造函数)。然后,这个临时对象再被用来初始化外面的 b(再次调用拷贝构造函数)。
如果不自己写拷贝构造函数,编译器会自动生成一个。
这个默认的拷贝构造函数做的事情很简单,叫做**“逐个成员初始化” (Member-wise Initialization)**。
- 对于普通数据类型 (如
int,double):直接复制值。 - 对于成员对象 (一个类包含另一个类的对象):调用那个成员对象的拷贝构造函数。
深拷贝 vs. 浅拷贝
在绝大多数情况下都不需要自己实现,隐式的拷贝构造函数够用了,但是有些情况有问题!
什么情况有问题?直接复制值,如果有个成员存的是指针,并且指针在初始化中才用new创建的对象。那这里复制一份后,指针并没有重新指向新的new的对象,而是指向了原来那个对象的成员!
比如:
class A{ int x,y; char *p;public: A(char *str) { x = 0; y = 0; p = new char[strlen(str)+1]; strcpy(p,str); } ~A() { delete [ ]p; p = NULL; } ...... // 其中没有定义拷贝构造函数};这样就有问题:
A a1("abcd");A a2(a1); // 调用隐式拷贝构造函数,用 a1 对 a2 进行初始化因此,隐式拷贝构造函数实现的,我们称呼它为浅拷贝(shallow copy)。
解决办法就是显式定义一个拷贝构造函数指针,来实现深拷贝(deep copy)。为新的对象的指针分配指向的空间,然后把牢对象指针指向的成员复制过来。
一旦你决定自己写拷贝构造函数,你就要对所有成员的拷贝行为负全责!编译器不会再像以前那么“智能”地帮你处理了。
编译器默认的拷贝构造函数,对于拷贝对象的时候,调用的是A的拷贝构造函数。如果自己写,在拷贝构造函数中如果没有明确指出a是怎么做的,编译器就会调用默认构造函数A()来创建A,有可能就不是真的拷贝了。
一个正确的实现是这样的:使用成员初始化列表 (Member Initializer List) 来实现这个拷贝:
B(const B& b) : a(b.a) { z = b.z;}四. 进一步讨论
1. 常成员函数(const)
一个对象(比如一个“银行账户”对象)通常有两种基本操作:
- 修改性操作:改变对象的状态。例如
deposit()(存款) 或withdraw()(取款) 会改变账户的balance(余额) 成员变量。 - 获取性操作:只读取对象的状态,不进行任何修改。例如
getBalance()(查询余额) 或getAccountHolder()(获取户主姓名)。
在编程时,我们非常希望能在语法层面就严格区分这两种操作。如果一个函数我们期望它只是“查询余额”,但因为疏忽在里面写了balance++的代码,这就会造成一个难以发现的 bug。
为了解决这个问题,C++ 提供了常成员函数机制。
WARNING在成员函数的声明和定义的末尾(括号后面,分号或大括号前面)加上
const关键字,这个函数就变成了常成员函数。
class A {public: int get_value() const; // 声明一个常成员函数 void set_value(int v); // 一个普通的成员函数private: int value;};
// 在类外定义时也要加上 constint A::get_value() const { // value = 10; // 错误!不可以在常成员函数内修改成员变量 return value; // 正确,只读取不修改}const 关键字就像一个承诺和一把锁。
- 对编译器的承诺:你告诉编译器:“我保证这个函数绝对不会修改对象的任何成员变量。”
- 编译器提供的锁:编译器会严格监督你的承诺。一旦它发现在常成员函数内部有任何试图修改成员变量的行为(如书上的
x++),它会立刻报错,从而在编译阶段就帮你发现了潜在的错误。
指针问题
但是有一个非常微妙但致命的“陷阱”:const 保证的是成员变量本身不被修改,但管不了成员变量(如果是个指针)所指向的内存。
class A { int x; char *p;public: void f() const { x = 10; // 错误!const 锁住了 x,不能修改。 p = new char[20]; // 错误!const 锁住了指针 p 本身,不能让它指向新的内存地址。
strcpy(p, "abcd"); // 正确!编译器允许这么做。 }};这是一个 C++ 语法的局限性。必须由写代码的人保证,在常成员函数中,不仅不修改指针成员变量本身,也不要通过它去修改它所指向的外部数据。
常量对象
什么是常量对象?就是一个在定义后就不希望被修改的对象。
const A my_const_object; // 一个常量对象C++ 规定:对于一个常量对象,你只能调用它的常成员函数,绝对不能调用任何普通(非 const)的成员函数。
在写函数时,我们经常把参数定义为常量引用 (const A&) 或常量指针 (const A\*)。这样做有两个好处:
- 效率高:避免了像
(A a)这样传值时发生一次拷贝构造,对于大对象来说能节省很多开销。 - 安全性高:向函数的调用者承诺“我这个函数不会修改你传进来的对象”,让接口更安全。
而一旦你把参数定义为 const,就意味着在这个函数里,你只能调用该参数的常成员函数了。所以,把所有“只读”操作的函数都定义为常成员函数,是一个非常好的编程习惯。
class A {public: void f() const { ... } // 常成员函数 void g() { ... } // 普通成员函数};
// func 的参数是一个指向常量对象的指针// 这意味着 func 承诺不会通过 pa 来修改它所指向的那个 A 对象void func(const A* pa) { pa->f(); // 正确!因为 f() 是 const 函数,可以被常量对象调用。 pa->g(); // 错误!因为 g() 是普通函数,编译器担心它会修改对象,所以禁止调用。}在C++中,const 关键字修饰的是它左边的东西(如果左边没有,就修饰右边的)
const A* pa 的意思是:“pa 是一个指向常量A的指针”。
A* const pa 的意思是,常量指针。const 在 * 的左边,所以它修饰的是 *,但为了方便理解,我们通常认为它修饰的是 pa 本身。
2. 静态数据成员(static)
静态数据成员
有的时候会有需求,我们想让一些数据被这个类的所有对象共享。
用全局变量也能实现数据共享,但那样做会破坏类的封装性,任何人都能随意修改,很不安全。静态成员就是为了解决这个问题而生的。
WARNING静态数据成员 (Static Data Member),在类的内部,用
static关键字声明的成员变量。
特点:
- 属于类,不属于对象:它不存放在任何一个对象里,而是单独存放在一块内存中,作为整个类的“公共财产”。
- 只有一份拷贝:无论你创建了 0 个还是 100 个对象,静态数据成员在内存中永远只有一份。
- 在类外定义和初始化:这是一个非常独特的语法规则。你必须在类的定义之外,对静态数据成员进行定义和初始化。
class A { int x, y; // 普通成员变量,每个对象都有一份 static int shared; // 静态数据成员,所有对象共享一份
public: void increase_all() { x++; y++; shared++; } int sum_all() const { return x+y+shared; } // ...};
// **关键步骤**:在类外进行定义和初始化int A::shared = 0;static int shared;:这只是一个声明,告诉编译器:“A 这个类里有一个叫 shared 的静态整数成员。”
int A::shared = 0;:这才是定义和初始化。它在内存中为 shared 分配了空间,并把它初始值设为 0。A:: 表明这个 shared 是属于类 A 的。这一行绝对不能省略!
静态成员函数
WARNING静态成员函数 (Static Member Function),是用
static关键字声明的成员函数。
特点:
- 为静态数据服务:它的主要目的就是操作静态数据成员。
- 没有
this指针:普通成员函数在被调用时,都有一个隐藏的this指针,指向调用它的那个对象。但静态成员函数属于类,不属于任何特定对象,所以它没有this指针。 - 访问限制:因为没有
this指针,它不能直接访问任何非静态成员(无论是变量还是函数)。它只能访问其他静态成员。
静态成员函数有两种调用方式,但更推荐第二种,因为它更清晰地体现了“属于类”的本质。
// 1. 通过对象调用 (不推荐)A a;a.set_shared(10);
// 2. 通过类名和作用域解析符::调用 (推荐)A::set_shared(20);cout << A::get_shared() << endl; // 输出 203. 友元
WARNING友元是 C++ 提供的一种机制,它允许一个类主动声明外部的某个特定函数或特定类是它的“朋友”,从而授权这个“朋友”可以直接访问该类的
private和protected成员。这是一种在数据保护和访问效率之间的折中方案。
友元函数
举例子,矩阵乘法的全局函数
// 不用友元的、低效的实现方式Vector multiply(const Matrix& m, const Vector& v) { // ... 检查维度是否匹配 ... Vector result(m.dimension_row()); for (int i = 0; i < m.dimension_row(); ++i) { int sum = 0; for (int j = 0; j < m.dimension_column(); ++j) { // 每次访问元素都要通过函数调用,效率较低 sum += m.element(i, j) * v.element(j); } result.element(i) = sum; } return result;}代码需要反复调用 element() 函数,这在计算密集型任务中会带来不必要的开销。
友元解决方案:
我们可以把 multiply 函数声明为 Matrix 类和 Vector 类的友元。这样,它就获得了直接访问这两个类私有数据的“特权”。
// --- 在 Vector.h (或类似文件) ---class Vector { // ... 其他成员 ... // 声明全局函数 multiply 是我的朋友 friend Vector multiply(const Matrix& m, const Vector& v);private: int *p_data; int num;};
// --- 在 Matrix.h (或类似文件) ---// 因为 Matrix 的友元函数 multiply 用到了 Vector,所以需要前向声明 Vector 类class Vector; // 前向声明
class Matrix { // ... 其他成员 ... // 声明全局函数 multiply 是我的朋友 friend Vector multiply(const Matrix& m, const Vector& v);private: int *p_data; int row, col;};现在,multiply 函数可以直接使用 . 操作符访问私有成员,就像它是类的成员函数一样。
// --- 在 multiply.cpp (或类似文件) ---Vector multiply(const Matrix& m, const Vector& v) { // ... 检查维度是否匹配,例如 if (m.col != v.num) ...
Vector result(m.row); // 创建结果向量
for (int i = 0; i < m.row; ++i) { int sum = 0; for (int j = 0; j < m.col; ++j) { // 因为是友元,所以可以高效地直接访问私有数据! sum += m.p_data[i * m.col + j] * v.p_data[j]; } result.p_data[i] = sum; // 同样可以直接访问 result 的私有数据 } return result;}friend 声明放在类的 public、private 还是 protected 部分,其效果是完全一样的。编译器并不关心你把它放在哪里。
友元类
我们刚才讲的是友元函数 (friend function),是把访问权限授予一个全局函数。
friend class B; 则是另一种形式,叫做友元类 (friend class)。它的意思是将访问权限授予一整个类。
如果在 A 类中声明 friend class B;,就意味着 B 类的所有成员函数都可以直接访问 A 类的 private 和 protected 成员。
最后,我们回顾一下的友元特性,这很重要:
- 友元非成员:友元函数不属于这个类,它只是被开了个“后门”。
- 不具对称性:如果在
A类中声明B是朋友 (friend class B;),不代表A也是B的朋友。授权是单向的。 - 不具传递性:
A是B的朋友,B是C的朋友,不代表A就是C的朋友。
4. 转移构造函数*
P240
5. 操作符重载
数学上看,操作符就是一种函数,其中,操作数是函数的参数,结果是函数的返回值。
在C++中,操作符重载(operator overloading)是通过函数实现的。
定义一个函数,以operator <操作符>的函数。
可以写成成员函数:
class Complex { double real, imag;public: Complex operator + (const Complex& x) const { Complex temp; temp.imag = imag + x.imag; return temp; }}也可以作为全局函数,然后变成友元。
class Complex {private: double real, imag;public: fridend Complex operator + (const Complex& x1, const Complex& x2);};
Complex operator + (const Complex& x1, const Complex& x2) { Complex temp; temp.real = x1.real + x2.real; temp.imag = x1.imag + x2.imag; return temp;}除了.,.*,::,?:,sizeof,其他都可以重载。
=重载
默认的对象赋值,A=B,其实是有一个隐的赋值操作符重载函数的,
和前面的拷贝构造函数一样,这里也有深拷贝和浅拷贝的问题。而且这里还不能构造,得先处理自己的内存问题。
A& A::operator=(const A& a) { // 1. 防止自我赋值 (a = a;) if (this == &a) { return *this; }
// 2. 释放自己原来的内存空间 delete [] p;
// 3. 根据对方的大小,为自己申请一块新的内存 p = new char[strlen(a.p) + 1];
// 4. 将对方内存中的“内容”复制到自己的新内存中 strcpy(p, a.p);
// 5. 复制其他非指针成员 x = a.x; y = a.y;
// 6. 返回对自己(左值)的引用,以支持链式赋值 (a = b = c;) return *this;}[]重载
class String { char *p;public: // ... char& operator[](int i) { // 操作符 "[]" 的重载函数 // 增加安全性检查,这是比普通数组更优秀的地方 if (i >= strlen(p) || i < 0) { cerr << "下标越界错误\n"; exit(-1); } return p[i]; }};