C++第4课:操作符

C++操作符(Operator)是用于执行程序代码运算的符号,其中算术操作符、关系操作符、逻辑操作符、赋值操作符、位操作符与C语言完全相同,这部分内容不再讲述,重点讲述C++操作符特有的操作。

流操作符

基本定义和用法


在 C++ 的输入输出体系中,最常用的两个流操作符是流插入运算符 “<<” 和流提取运算符 “>>” 。“<<” 负责将数据从变量传输到输出流中,如标准输出流cout,它通常对应着显示器,也可以是文件流,用于将数据写入文件。而 “>>” 则从输入流中获取数据并传输给变量,常如标准输入流cin,主要用于接收来自键盘的输入数据,当然也能从文件流中读取数据。简单来说,“<<” 用于输出数据,让程序的内容得以呈现;“>>” 用于输入数据,使程序能够获取外部的信息 。这两个流操作符是 C++ 进行数据输入输出的基础工具,在程序中频繁使用。
下面通过一段简单的代码示例,来看看流操作符的用法。假设我们要从键盘读取一个人的年龄和工资信息,并将其输出显示 :​

#include <iostream>​
using namespace std;​
​
int main() {​
int age;​
double salary;​
​
// 输入数据​
cout << "请输入你的年龄和工资:";​
cin >> age >> salary;​
​
// 输出数据​
cout << "你的年龄是:" << age << endl;​
cout << "你的工资是:" << salary << endl;​
​
return 0;​
}​
​

在上述代码中,首先使用cout和 “<<” 输出提示信息,引导用户输入数据。然后通过cin和 “>>” 从键盘读取用户输入的年龄和工资数据,并分别存储到age和salary变量中。最后,使用cout和 “<<” 将读取到的数据输出显示。

流操作符的重载机制


内置类型支持​
C++已经为我们处理好了基本类型的流操作符重载。对于像int、double、char等基本数据类型,我们可以直接使用 “<<” 和 “>>” 进行输入输出操作,无需手动重载。例如,当我们使用cout << 10;时,背后调用的就是标准库中为int类型重载的 “<<” 操作符函数 。这些典型的函数原型如下:​

ostream& operator<<(ostream& os, int value);​
istream& operator>>(istream& is, int& value);​

对于double类型,同样有对应的重载函数:​

ostream& operator<<(ostream& os, double value);​
istream& operator>>(istream& is, double& value);​

以此类推,对于其他基本类型,也都有类似的重载函数。

自定义类型重载步骤​


当涉及到自定义类型时,就需要手动重载流操作符,下面以一个简单的日期类Date为例,详细讲解自定义类型重载流操作符的步骤。​
(1)自定义类型

class Date {​
private:​
int year;​
int month;​
int day;​
public:​
// 构造函数​
Date(int y, int m, int d) : year(y), month(m), day(d) {}​
// 友元声明​
friend ostream& operator<<(ostream& os, const Date& d);​
friend istream& operator>>(istream& is, Date& d);​
};​


在Date类中,声明了两个友元函数,分别用于重载输出流操作符 “<<” 和输入流操作符 “>>” 。使用友元函数,是因为这样可以让函数访问类的私有成员,方便在重载函数中对日期的年、月、日进行操作。如果不使用友元函数,由于操作符重载函数的第一个参数不是类对象本身(this指针所指向的对象),无法直接访问类的私有成员,就需要通过类的公有接口来访问,这会增加代码的复杂性和冗余度。​

流操作符重载

ostream& operator<<(ostream& os, const Date& d) {​
os << d.year << "-" << d.month << "-" << d.day;​
return os;​
}​


在上面的函数中,将日期对象d的年、月、日按照指定的格式输出到输出流os中,然后返回输出流对象os,这样做是为了支持链式调用,例如如cout << date1 << " " << date2;,如果不返回流对象的引用,就无法实现这种连续的输出操作 。​
实现输入流操作符 “>>” 的重载,同时加入一些简单的输入校验:​

istream& operator>>(istream& is, Date& d) {​
char dash1, dash2;​
is >> d.year >> dash1 >> d.month >> dash2 >> d.day;​
if (dash1 != '-' || dash2 != '-' || d.month < 1 || d.month > 12) {​
is.setstate(ios::failbit); // 标记错误状态​
}​
return is;​
}​


在上面的函数中,首先从输入流is中读取年、月、日的数据,同时读取分隔符 “-” 。然后进行输入校验,如果分隔符不是 “-”,或者月份不在 1 到 12 的范围内,就将输入流的状态设置为错误状态,这样后续的输入操作可以根据流的状态来判断是否输入正确 。最后返回输入流对象is,同样是为了支持链式调用,例如cin >> date1 >> date2;。

成员访问操作符

在C++中,成员访问操作符主要用于类和结构体的成员访问,主要有两个成员访问操作符:点操作符(.)和箭头操作符(->)。

点操作符


点操作符在 C++ 中用于访问对象的成员,无论是成员变量还是成员函数。假设我们正在开发一个简单的图形绘制库,定义一个Circle类来表示圆:​

class Circle {​
public:​
double radius;​
double x, y; // 圆心坐标​
double area() {​
return 3.14159 * radius * radius;​
}​
};​


当我们创建一个Circle对象时,就可以使用点操作符来访问它的成员:​

Circle myCircle;​
myCircle.radius = 5.0;​
myCircle.x = 10.0;​
myCircle.y = 10.0;​
double circleArea = myCircle.area();​


在上述代码中,myCircle.radius和myCircle.x、myCircle.y是通过点操作符访问对象的成员变量,而myCircle.area()则是通过点操作符调用对象的成员函数。

箭头操作符

箭头操作符(->)主要用于通过指针访问对象的成员,它的语法形式为指针变量->成员名。假设我们正在编写一个简单的数据库连接类

DatabaseConnection :​
​
class DatabaseConnection {​
public:​
std::string host;​
int port;​
void connect() {​
std::cout << "连接到数据库:" << host << ":" << port << std::endl;​
}​
};​


当使用指针来操作这个类时:​

DatabaseConnection* dbConn = new DatabaseConnection;​
dbConn->host = "localhost";​
dbConn->port = 3306;​
dbConn->connect();​
delete dbConn;​


这里的dbConn是一个指向DatabaseConnection对象的指针,通过dbConn->host、dbConn->port来访问对象的成员变量,通过dbConn->connect()来调用对象的成员函数。与点操作符不同的是,箭头操作符的左边必须是一个指针,而点操作符的左边是一个具体的对象。 如果我们这样写DatabaseConnection dbConn;,那么访问成员就需要使用点操作符dbConn.host、dbConn.port和dbConn.connect() 。

两者的区别

(1)使用方式差异
点操作符和箭头操作符最明显的区别就在于它们的操作数类型不同。点操作符(.)用于访问对象的成员,其左边必须是一个对象,无论是类的实例对象还是结构体对象。例如:​

​
class Book {​
public:​
std::string title;​
std::string author;​
void displayInfo() {​
std::cout << "书名: " << title << ", 作者: " << author << std::endl;​
}​
};​
​
Book myBook;​
myBook.title = "C++ Primer";​
myBook.author = "Stanley Lippman";​
myBook.displayInfo(); ​


在上面的例子中,myBook是Book类的一个对象,通过点操作符可以直接访问其成员变量title、author和成员函数displayInfo 。
箭头操作符(->)用于通过指针访问对象的成员,其左边必须是一个指针。例如:​

Book* pBook = new Book;​
pBook->title = "Effective C++";​
pBook->author = "Scott Meyers";​
pBook->displayInfo();​
delete pBook; ​


这里pBook是指向Book对象的指针,通过箭头操作符来访问指针所指向对象的成员。如果错误地使用点操作符,如pBook.title,编译器会报错,因为点操作符期望的是一个对象,而不是指针。同样,如果使用箭头操作符在对象上,如myBook->title,也会导致编译错误,因为箭头操作符要求左边是指针。
(2)内存和寻址差异
从内存和寻址来看,点操作符和箭头操作符的工作方式也有所不同。当使用点操作符访问对象成员时,编译器会直接在对象的内存区域中找到对应的成员。因为对象在内存中是一块连续的存储区域,成员变量和成员函数的地址在编译时就已经确定,通过对象的首地址加上成员的偏移量,就可以直接访问到成员。例如,对于前面定义的Book类对象myBook ,假设myBook的首地址为0x1000,title成员变量在对象中的偏移量为0(假设title是第一个成员变量),那么访问myBook.title时,编译器会直接从地址0x1000处开始读取title的数据。​
而箭头操作符的寻址过程则稍微复杂一些。当使用箭头操作符通过指针访问对象成员时,首先要解引用指针,得到指针所指向对象的内存地址,然后再按照与点操作符相同的方式,在该对象的内存区域中访问成员。例如对于指针pBook ,假设pBook的值为0x2000,这是它所指向的Book对象的地址。当执行pBook->title时,首先解引用pBook,得到对象的地址0x2000,然后从地址0x2000处开始,按照title成员变量在对象中的偏移量来访问title的数据。本质上,pBook->title等价于(*pBook).title ,先通过*pBook解引用指针得到对象,再使用点操作符访问对象的成员。

new和delete操作符

在 C++ 中,new和delete操作符用于在堆上分配和释放内存。new操作符用于在堆上分配内存,delete操作符用于释放由new分配的内存。当我们使用new分配的内存不再需要时,就应该使用delete来释放,以避免内存泄漏。

new操作符


new操作符用于在堆上分配内存。其基本语法为:
type* pointer = new type;
这里type是要分配内存的数据类型,例如int、double、自定义的类等,pointer是一个指针,用于指向分配的内存地址。例如,当我们需要为一个int型变量分配内存时,可以这样写:
int* numPtr = new int;​
这行代码在堆上为一个int型变量分配了内存,并将该内存的地址赋给了numPtr指针。之后,可以通过*numPtr来访问和操作这块内存,例如给它赋值:​

*numPtr = 10;​

如果要分配一个数组的内存,new的语法变为:
type* arrayPtr = new type[size];
其中size表示数组元素的个数。例如分配一个包含 5 个int型元素的数组内存:​
int* arrPtr = new int[5];​
通过arrPtr[i]可以访问和操作数组中的每个元素,例如:​​
arrPtr[2] = 20;​

delete操作符


delete操作符用于释放由new分配的内存。当我们使用new分配的内存不再需要时,就应该使用delete来释放,以避免内存泄漏。​
对于单个变量的内存释放,语法为:
delete pointer;
例如,释放前面分配的int型变量的内存:​
delete numPtr;​
而对于数组内存的释放,必须使用delete[],语法为:
delete[] arrayPtr;
如果错误地使用delete来释放数组内存,会导致内存释放不完整,引发内存泄漏等问题。例如,释放前面分配的数组内存:​
delete[] arrPtr;​

应用案例


下面通过一个代码案例,来展示new和delete在实际编程中的使用:​

#include <iostream>​
​
int main() {​
// 动态分配一个int型变量​
int* num = new int;​
*num = 100;​
std::cout << "num的值为:" << *num << std::endl;​
​
// 动态分配一个包含5个元素的int型数组​
int* arr = new int[5];​
for (int i = 0; i < 5; ++i) {​
arr[i] = i * 2;​
}​
std::cout << "数组元素为:";​
for (int i = 0; i < 5; ++i) {​
std::cout << arr[i] << " ";​
}​
std::cout << std::endl;​
​
// 释放内存​
delete num;​
delete[] arr;​
​
return 0;​
}​
​

在这个案例中,首先使用new分配了一个int型变量和一个包含 5 个元素的int型数组的内存,然后对它们进行了赋值和操作,最后使用delete和delete[]分别释放了它们的内存。