目录
[1.1 为什么要对运算符重载](#1.1 为什么要对运算符重载)
[1.2 对运算符重载的方法](#1.2 对运算符重载的方法)
[1.3 重载运算符的规则](#1.3 重载运算符的规则)
[1.4 运算符重载函数作为类成员函数和友元函数](#1.4 运算符重载函数作为类成员函数和友元函数)
[1.5 重载双目运算符](#1.5 重载双目运算符)
[1.6 重载单目运算符](#1.6 重载单目运算符)
[1.7 ★重载流插入运算符和流提取运算符☆](#1.7 ★重载流插入运算符和流提取运算符☆)
[1.7.1 重载流插入运算符"<<"](#1.7.1 重载流插入运算符“<<”)
[1.7.2 重载流提取运算符">>"](#1.7.2 重载流提取运算符“>>”)
[1.8 有关运算符重载的归纳](#1.8 有关运算符重载的归纳)
[1.9 不同类型数据间的转换](#1.9 不同类型数据间的转换)
[1.9.1 标准类型数据间的转换](#1.9.1 标准类型数据间的转换)
[1.9.2 把其他类型数据转换为类对象------用转换构造函数](#1.9.2 把其他类型数据转换为类对象——用转换构造函数)
[1.9.3 将类对象转换为其他数据类型------用类型转换函数](#1.9.3 将类对象转换为其他数据类型——用类型转换函数)
[2.1 核心概念](#2.1 核心概念)
[2.3 参数类型差异](#2.3 参数类型差异)
[2.4 参数数量差异](#2.4 参数数量差异)
[2.6 函数重载与模板](#2.6 函数重载与模板)
[2.7 重载决议(Overload Resolution)](#2.7 重载决议(Overload Resolution))
[2.8 常见问题与注意事项](#2.8 常见问题与注意事项)
二、重载
C++ 重载(Overloading)是一种允许在同一作用域内定义多个同名函数或运算符的特性,通过参数列表或操作数类型的差异来区分它们。重载机制提高了代码的可读性和灵活性,使程序员可以用相同的名称处理不同类型的任务。
1、运算符重载
1.1 为什么要对运算符重载
C++ 引入运算符重载机制主要是为了增强代码的可读性 和可维护性,让自定义类型(如类和结构体)能够像内置类型一样自然地参与运算。
示例:"<<"是位运算中的移位运算符,但在输出操作中又是与流对象cout配合使用的流插入运算符。
以下是几个核心原因:
- 语法一致性
通过重载运算符,可以让自定义类型的操作与内置类型(如
int
、double
)的语法保持一致。例如:// 内置类型相加
int a = 1, b = 2;
int sum = a + b;
// 自定义复数类型相加(通过运算符重载实现)
Complex c1(1, 2), c2(3, 4);
Complex result = c1 + c2; // 自然直观的语法
若没有运算符重载,复数加法可能需要写成
result = c1.add(c2)
,这种写法在数学表达式中会显得冗长。
- 代码可读性
运算符重载使代码更符合数学或领域特定的表达习惯。例如:
矩阵运算 :
matrix1 + matrix2
比matrix1.add(matrix2)
更直观。字符串拼接 :
str1 + str2
比str1.concat(str2)
更符合直觉。逻辑操作 :
obj1 && obj2
比obj1.and(obj2)
更简洁。
- 增强类型抽象
运算符重载允许自定义类型完全融入 C++ 的类型系统,支持泛型编程(如 STL 算法)。例如:
std::vector<Complex> numbers = {c1, c2};
Complex total;
for (const auto& num : numbers) {
total += num; // 若未重载+=,无法直接使用
}
- 保持 STL 兼容性
标准库中的容器和算法依赖运算符重载来实现功能:
operator<
:用于排序(如std::sort
)和关联容器(如std::map
)。
operator==
:用于查找(如std::find
)。
operator<<
:用于流输出(如std::cout << obj
)。若自定义类型未重载这些运算符,将无法直接与 STL 组件配合使用。
- 领域特定语言(DSL)
在科学计算、图形处理等领域,运算符重载可创建接近数学符号的代码:
// 线性代数库中的向量运算
Vector3 a(1, 2, 3), b(4, 5, 6);
Vector3 c = a + b * 2.0; // 类似数学表达式
- 避免命名冲突
运算符名称(如
+
、*
)是预定义的,无需为相似功能的操作发明新名称(如add
、multiply
),减少了命名空间的污染。注意事项
尽管运算符重载带来便利,但需遵循以下原则:
保持语义一致性 :重载运算符的行为应与内置类型类似(例如
+
应满足交换律)。避免滥用 :不建议重载不符合直觉的运算符(如用
*
实现字符串拼接)。遵循标准规则 :部分运算符(如
.
、::
)不可重载,且不能改变运算符优先级。
1.2 对运算符重载的方法
运算符重载是通过定义函数实现的。运算符重载实质上是函数的重载。
重载运算符的函数一般格式: 返回类型 operator 运算符名称(形参表) {对运算符的重载处理}
函数名是由operator和运算符组成的
operator是关键字,是专门用于定义重载运算符的函数的。运算符名称就是C++已有的运算符。
示例:
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex()
{
real = 0;
imag = 0;
}
Complex(double r, double i)
{
real = r;
imag = i;
}
// 声明运算符重载函数
Complex operator+(const Complex &c2) const;
void display() const;
};
// 定义运算符重载函数
Complex Complex::operator+(const Complex &c2) const
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
// 定义display函数
void Complex::display() const
{
cout << "(" << real << "," << imag << "i)" << endl; //复数形式
}
int main()
{
Complex c1(3, 4), c2(5, -10), c3;
c3 = c1 + c2;
cout << "c1=";
c1.display();
cout << "c2=";
c2.display();
cout << "c1+c2="; //运算符"+"用于复数运算
c3.display();
return 0;
}
注意:
运算符被重载后,其原有的功能仍然保存,没有丧失或改变。
不要把运算符重载看作C++的一个具体方法,说可有可无或可用可不用的。由于常用的运算符只能用于标准类型,而在面向对象程序中,程序设计者自己建立了一些类,往往需要将这些类对象中的数据进行运算符重载,以方便对象的操作。
1.3 重载运算符的规则
C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载。
C++允许重载运算符 C++绝大多数的运算符是允许重载的
类别 运算符 双目算术运算符 +(加), -(减), *(乘), /(除), %(取模) 关系运算符 ==(等于), !=(不等于), <(小于), >(大于), <=(小于等于), >=(大于等于) 逻辑运算符 ||(逻辑或), &&(逻辑与), !(逻辑非) 单目运算符 +(正), -(负), *(指针), &(取地址) 自增自减运算符 ++(自增), --(自减) 位运算符 |(按位或), &(按位与), ~(按位取反), ^(按位异或), <<(左移), >>(右移) 赋值运算符 =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= 空间申请与释放 new, delete, new[], delete[] 其他运算符 ()(函数调用), ->(成员访问), ->*(成员指针访问), ,(逗号), 不能重载的运算符有5个
. 成员访问运算符 :: 成员指针访问运算符 * 域运算符 sizeof 长度运算符 ?: 条件运算符
重载不能改变运算符运算对象(即操作数)的个数
重载不能改变运算符的优先级别。
重载不能改变运算符的结合性
重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数。
重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用)。 也就是说,参数不能全部都是C++的标准类型,以防止用户修改用于标准类型数据的运算符的性质。
用于类对象的运算符一般必须重载,但是有两个例外,运算符"="和"&"不必用户重载
就理论而言,可以将一个运算符重载为执行任意的操作。但为了代码的清晰明了,应当使重载运算符的功能类似于该运算符作用于标准内型数据时所实现的功能。
1.4 运算符重载函数作为类成员函数和友元函数
对运算符重载的函数有两种处理方法:
-
把运算符重载的函数作为类的成员函数 示例:
#include <iostream> using namespace std; class Complex { private: double real; double imag; public: Complex() { real = 0; imag = 0; } Complex(double r, double i) { real = r; imag = i; } // 声明运算符重载函数 Complex operator+(const Complex &c2) const; void display() const; }; // 定义运算符重载函数 Complex Complex::operator+(const Complex &c2) const { Complex c; c.real = real + c2.real; c.imag = imag + c2.imag; return c; } // 定义display函数 void Complex::display() const { cout << "(" << real << "," << imag << "i)" << endl; //复数形式 } int main() { Complex c1(3, 4), c2(5, -10), c3; c3 = c1 + c2; cout << "c1="; c1.display(); cout << "c2="; c2.display(); cout << "c1+c2="; //运算符"+"用于复数运算 c3.display(); return 0; }
-
运算符重载的函数不是类的成员函数(可以是一个普通函数),在类中把它声明为友元。 示例:
#include<iostream> using namespace std; class Complex{ public: Complex() { real=0; imag=0; } Complex(double r,double i) { real=r; imag=i; } friend Complex operator+(Complex &c1,Complex &c2); //定义运算符“+”重载函数 void display(); private: double real; double imag; }; Complex operator+(Complex &c1,Complex &c2) { return Complex(c1.real+c2.real,c1.imag+c2.imag); } void Complex::display() { cout << "(" << real << "," << imag << "i)" << endl; } int main() { Complex c1(3, 4), c2(5, -10), c3; c3 = c1 + c2; cout << "c1="; c1.display(); cout << "c2="; c2.display(); cout << "c1+c2="; //运算符"+"用于复数运算 c3.display(); return 0; }
-
如果将运算符重载函数作为成员函数,它可以通过this指针自由地访问本类的数据成员,因此可以少写一个函数的参数
-
将双目运算符重载为友元函数时,由于友元函数不是该类的成员函数,因此在函数的形参列表中必须有两个参数不能省略。
-
什么情况下使用成员函数方式?什么情况下用友元函数方式? 参考: ①C++规定,赋值运算符=、下标运算符[ ]、函数调用运算符()、成员运算符->必须作为成员函数
②流插入<<和流提取>>运算符、类型转换运算符不能定义为类成员函数,只能作为友元函数。
③一般将单目运算符和复合运算符(+=,-=,/=,*=,&=,!=,`=,%=,>>=,<<=)重载为成员函数。
④一般将双目运算符重载为友元函数。
-
1.5 重载双目运算符
示例:声明一个字符串类String,用来存放不定长的字符串,重载运算符"==",">"和"<"使它们能用于两个字符串的等于、小于和大于的比较运算
过程一:先建立一个String类
#include<iostream>
using namespace std;
class String
{
public:
String() //定义默认构造函数
{
p=NULL;
}
String(char *str); //声明构造函数
void display();
private:
char *p; //字符型指针,用于指向字符串
};
String::String(char *str)
{
p=str;
}
void String::display()
{
cout<<p;
}
int main()
{
String string1("Hello"),string2("Book");
string1.display();
cout<<endl;
string2.display();
return 0;
}
过程二:在此基础上增加其它的必要内容。现在增加对运算符重载的部分
#include<iostream>
using namespace std;
class String
{
public:
String() //定义默认构造函数
{
p=NULL;
}
String(char *str); //声明构造函数
friend bool operator>(String &string1,String &string2); //声明运算符函数为友元函数
void display();
private:
char *p; //字符型指针,用于指向字符串
};
String::String(char *str)
{
p=str;
}
void String::display()
{
cout<<p;
}
bool operator>(String &string1,String &string2)
{
if(strcmp(string1.p,string2.p)>0)
return true;
else
return false;
}
int main()
{
String string1("Hello"),string2("Book");
String.display;
string1.display();
cout<<endl;
string2.display();
return 0;
}
过程三:扩展到3个运算符重载
-
在String类中声明3个友元成员函数
-
在类外分别定义3个运算符重载函数
-
再修改主函数
#include<iostream>
#include<cstring> // 添加缺失的头文件用于strcmp
using namespace std;
class String
{
public:
String()
{
p=NULL;
}
String(char *str);
friend bool operator>(String &string1,String &string2);
friend bool operator<(String &string1,String &string2);
friend bool operator==(String &string1,String &string2); // 修正:将=改为==
void display();
private:
char *p;
};
String::String(char *str)
{
p=str;
}
void String::display()
{
cout<<p;
}
bool operator>(String &string1,String &string2)
{
if(strcmp(string1.p,string2.p)>0)
return true;
else
return false;
}
bool operator<(String &string1,String &string2)
{
if(strcmp(string1.p,string2.p)<0)
return true;
else
return false;
}
bool operator==(String &string1,String &string2) // 修正:将=改为==
{
if(strcmp(string1.p,string2.p)==0) // 修正:将=改为==
return true;
else
return false;
}
int main()
{
char s1[] = "Hello"; // 使用字符数组替代字符串字面量
char s2[] = "Book";
char s3[] = "Computer";
String string1(s1),string2(s2),string3(s3); // 使用字符数组初始化
cout<<(string1>string2)<<endl;
cout<<(string1<string2)<<endl;
cout<<(string1==string2)<<endl; // 修正:将=改为==
return 0;
}
过程四:进一步修缮,是输出结果更加直观
#include<iostream>
#include<cstring> // 添加缺失的头文件用于strcmp
using namespace std;
class String
{
public:
String()
{
p=NULL; // 修正:中文分号改为英文分号
}
String(char *str);
friend bool operator>(String &string1,String &string2);
friend bool operator<(String &string1,String &string2);
friend bool operator==(String &string1,String &string2); // 修正:将=改为==
void display();
private:
char *p;
};
String::String(char *str)
{
p=str;
}
void String::display()
{
cout<<p;
}
bool operator>(String &string1,String &string2)
{
return strcmp(string1.p,string2.p)>0; // 简化写法
}
bool operator<(String &string1,String &string2)
{
return strcmp(string1.p,string2.p)<0; // 简化写法
}
bool operator==(String &string1,String &string2) // 修正:将=改为==
{
return strcmp(string1.p,string2.p)==0; // 修正:将=改为==,简化写法
}
void Compare(String &string1,String &string2) // 函数名首字母大写,与声明保持一致
{
if(operator>(string1,string2)) // 修正:移除=1,直接使用布尔值
{
string1.display();
cout<<">";
string2.display();
}
else if(operator<(string1,string2)) // 修正:移除=1,合并else if
{
string1.display();
cout<<"<";
string2.display();
}
else if(operator==(string1,string2)) // 修正:将=改为==,移除=1
{
string1.display();
cout<<"=";
string2.display();
}
cout<<endl;
}
int main()
{
char s1[] = "Hello"; // 使用字符数组替代字符串字面量
char s2[] = "Book";
char s3[] = "Computer";
char s4[] = "Hello";
String string1(s1),string2(s2),string3(s3),string4(s4); // 修正:移除多余逗号
Compare(string1,string2);
Compare(string2,string3);
Compare(string3,string4);
return 0;
}
指导思想:先搭框架,逐步扩充,由简到繁,最后完善。遍编程,遍调试,遍扩充。
1.6 重载单目运算符
单目运算符只有一个操作数。其重载方式与双目运算符类似。但由于单目运算符只有一个操作数,因此运算符重载只有一个参数,如果运算符重载函数作为成员函数,则还可以省略此函数。
示例:有一个Time类,包含成员函数minute(分钟)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0起算。要求输出分钟和秒的值。
#include<iostream>
using namespace std;
class Time
{
public:
Time() //默认构造函数
{
minute=0;
sec=0;
}
Time(int m,int s):minute(m),sec(s){} //构造函数重载
Time operator++(); //声明运算符重载函数成员
void display()
{
cout<<minute<<":"<<sec<<endl;
}
private:
int minute;
int sec;
};
Time Time::operator++()
{
if(++sec>=60)
{
sec-=60;
++minute;
}
return *this; //返回当前对象值
}
int main()
{
Time time1(34,40);
for(int i=0;i<61;i++)
{
++time1;
time1.display();
}
return 0;
}
1.7 ★重载流插入运算符和流提取运算符☆
C++的流插入运算符"<<"和流提取运算符">>"是C++编译系统在类库中提供的。
所有C++编译系统都在其类库在提供数据流istream和输出流ostream。cin和cout分别是其对象。
用户自己定义的类型的数据(如类对象),是不能直接用输入输出的。如果想用他们输入和输出自己声明的类型和数据,必须对他们重载。
对"<<"和">>"重载的函数形式如下: istream & operator>>(istream &,自定义类 &); ostream & operator>>(ostream &,自定义类 &);
注意:只能将重载">>"和"<<"的函数作为友元函数,而不能将它们定义为成员函数。
成员函数 :操作符左侧必须是类的实例,这和
>>
、<<
的常规使用方式不相符。友元函数 :能够让操作符左侧是流对象(
istream
/ostream
),符合正常的语法习惯,并且还能访问类的私有成员。
1.7.1 重载流插入运算符"<<"
在 C++ 里,流插入运算符<<
一般是和cout
搭配使用来输出数据的。借助运算符重载这一特性,我们能够重新定义<<
对于自定义类型的行为,从而让它可以直接输出自定义类的对象。下面详细介绍流插入运算符重载的相关内容:
基本语法
重载
<<
时,要把它定义成友元函数 或者普通函数 ,因为该运算符的左侧是ostream
对象,并非我们自定义类的对象。// 友元函数形式(常用)
friend std::ostream& operator<<(std::ostream& os, const CustomType& obj);
// 普通函数形式(需要提供公共访问接口)
std::ostream& operator<<(std::ostream& os, const CustomType& obj);
关键要点
返回类型 :返回
std::ostream&
,这样就能支持链式输出,比如cout << a << b;
。参数设置
:
第一个参数
std::ostream& os
:代表输出流对象,像cout
就是常见的例子。第二个参数
const CustomType& obj
:是自定义类对象的引用,使用const
可以避免对象被修改。函数实现:在函数内部,要按照特定的格式把对象的数据写入到流中,最后返回流对象。
友元函数实现示例
下面以复数类为例,展示如何重载
<<
:#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 声明友元函数
friend ostream& operator<<(ostream& os, const Complex& c);
};
// 定义友元函数
ostream& operator<<(ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << "i)";
return os;
}
int main() {
Complex c(3.5, 2.8);
cout << "Complex number: " << c << endl; // 输出: Complex number: (3.5, 2.8i)
return 0;
}
普通函数实现示例
若不想使用友元函数,那就得通过公共成员函数来访问对象的数据:
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 提供公共访问接口
double getReal() const { return real; }
double getImag() const { return imag; }
};
// 普通函数重载
ostream& operator<<(ostream& os, const Complex& c) {
os << "(" << c.getReal() << ", " << c.getImag() << "i)";
return os;
}
常见应用场景
自定义类的输出:可以按照我们的需求,自定义类对象的输出格式,像日期类、矩阵类等都能使用。
链式输出
:能够实现连续输出多个对象,例如:
cout << "Result: " << add(a, b) << " + " << c << endl;
注意事项
不能重载为成员函数 :因为成员函数的左侧操作数必须是当前类的对象,而
<<
的左侧是ostream
对象,所以只能用友元函数或者普通函数来重载。避免修改流状态 :一般情况下,不要对流的状态(如
width
、precision
)进行修改,除非有特殊需求。格式控制 :可以使用
os << std::fixed << std::setprecision(2)
等方式来控制输出的格式。进阶应用
如果要实现更复杂的输出功能,可以结合自定义格式控制符一起使用:
// 自定义格式控制符示例
ostream& scientific(ostream& os) {
os << std::scientific;
return os;
}
// 使用自定义控制符
cout << scientific << 123.456 << endl; // 科学计数法输出
通过重载流插入运算符,能够极大地提升代码的可读性和易用性,让自定义类型的输出变得更加自然。
示例:复数相加
#include<iostream>
using namespace std;
// 复数类定义,包含实部和虚部
class Complex
{
public:
// 默认构造函数,初始化复数为0+0i
Complex()
{
real=0;
imag=0;
}
// 带参数的构造函数,初始化复数为r+ii
Complex(double r,double i)
{
real=r;
imag=i;
}
// 运算符"+"重载为成员函数
// 参数:Complex& c2 - 右侧操作数的引用
// 返回:两个复数相加后的新复数对象
// 调用形式:c1 + c2 等价于 c1.operator+(c2)
Complex operator+(Complex &c2);
// 运算符"<<"重载为友元函数
// 注意:必须作为友元函数重载,因为左侧操作数是ostream对象
// 参数:ostream& - 输出流对象,Complex& - 待输出的复数对象
// 返回:输出流对象的引用,支持链式输出
friend ostream& operator<<(ostream&,Complex&);
private:
double real; // 实部
double imag; // 虚部
};
// 运算符"+"重载函数的实现
// 访问方式:通过类作用域解析符::访问类的成员函数
Complex Complex::operator+(Complex &c2)
{
// 创建并返回一个新的复数对象,其实部和虚部分别为两个复数对应部分的和
// 注意:这里的real等价于this->real,即当前对象的实部
return Complex(real+c2.real,imag+c2.imag);
}
// 运算符"<<"重载函数的实现
// 注意:这不是类的成员函数,但通过友元关系可以访问类的私有成员
ostream & operator<<(ostream & output,Complex& c)
{
// 按照"(实部+虚部i)"的格式输出复数
// 例如:2+3i 会输出为 (2+3i)
output<<"("<<c.real<<"+"<<c.imag<<"i)"<<endl;
// 返回输出流对象的引用,支持链式调用,如 cout << a << b;
return output;
}
int main()
{
// 创建复数对象并初始化
Complex c1(2,4),c2(6,10),c3;
// 调用重载的"+"运算符进行复数加法
// 等价于 c3 = c1.operator+(c2);
c3=c1+c2;
// 调用重载的"<<"运算符输出复数
// 等价于 operator<<(cout, c3);
cout<<c3;
return 0;
}
- 成员函数重载
+
运算符
为什么用成员函数? :
+
运算符的左侧操作数是当前类的对象,因此可以定义为成员函数。调用机制 :当执行
c1 + c2
时,实际上调用的是c1.operator+(c2)
。参数说明 :只需要一个参数表示右侧操作数,左侧操作数由
this
指针隐式传递。
- 友元函数重载
<<
运算符
为什么用友元函数? :
<<
运算符的左侧操作数是ostream
对象,右侧才是自定义类对象,因此必须使用友元函数或普通函数重载。参数说明 :第一个参数是
ostream&
类型,表示输出流;第二个参数是复数对象的引用。返回值 :返回
ostream&
类型,以便支持链式输出,如cout << a << b;
。
- 友元函数的必要性
成员函数要求左侧操作数必须是当前类的对象,而
<<
的左侧是ostream
对象,因此无法作为成员函数重载。通过友元函数,可以在不破坏封装性的前提下访问类的私有成员。
- 重载函数中的访问权限
在
operator+
中,可以直接访问c2.real
和c2.imag
,因为这是类的成员函数,可以访问同类对象的私有成员。在
operator<<
中,由于是友元函数,也可以直接访问复数对象的私有成员c.real
和c.imag
。
1.7.2 重载流提取运算符">>"
在 C++ 中,流提取运算符>>
通常与cin
配合使用来读取输入数据。通过运算符重载,我们可以自定义>>
对于自定义类型的行为,使其能够直接从输入流中读取数据到自定义类的对象中。以下是关于流提取运算符重载的详细介绍:
基本语法
重载
>>
时,需将其定义为友元函数 或普通函数 ,因为该运算符的左侧是istream
对象,而非自定义类的对象。// 友元函数形式(常用)
friend std::istream& operator>>(std::istream& is, CustomType& obj);
// 普通函数形式(需要提供公共设置接口)
std::istream& operator>>(std::istream& is, CustomType& obj);
关键要点
返回类型 :返回
std::istream&
,以支持链式输入,例如cin >> a >> b;
。参数设置
:
第一个参数
std::istream& is
:表示输入流对象,如cin
。第二个参数
CustomType& obj
:是自定义类对象的非 const 引用,因为需要修改对象的值。函数实现:从流中读取数据并赋值给对象的成员变量,同时处理可能的输入错误,最后返回流对象。
友元函数实现示例
以下以复数类为例,展示如何重载
>>
:#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 声明友元函数
friend istream& operator>>(istream& is, Complex& c);
};
// 定义友元函数
istream& operator>>(istream& is, Complex& c) {
char ch;
// 假设输入格式为: (实部, 虚部i)
if (is >> ch && ch == '(') {
if (is >> c.real >> ch && ch == ',') {
if (is >> c.imag >> ch && ch == 'i') {
if (is >> ch && ch == ')') {
return is; // 成功读取完整格式
}
}
}
}
// 若格式不匹配,设置流的错误状态
is.setstate(ios::failbit);
return is;
}
int main() {
Complex c;
cout << "Enter complex number (format: (实部, 虚部i)): ";
cin >> c;
if (cin.fail()) {
cout << "输入格式错误!" << endl;
} else {
cout << "读取的复数: " << c << endl;
}
return 0;
}
普通函数实现示例
若不想使用友元函数,需通过公共成员函数设置对象的数据:
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 提供公共设置接口
void setValue(double r, double i) {
real = r;
imag = i;
}
};
// 普通函数重载
istream& operator>>(istream& is, Complex& c) {
double r, i;
char ch;
if (is >> ch && ch == '(' && is >> r >> ch && ch == ',' &&
is >> i >> ch && ch == 'i' && is >> ch && ch == ')') {
c.setValue(r, i);
return is;
}
is.setstate(ios::failbit);
return is;
}
常见应用场景
自定义类的输入:如日期类、矩阵类等,可按特定格式读取数据。
批量输入
:支持连续读取多个对象,例如:
vector<Complex> nums(3);
for (auto& num : nums) {
cin >> num;
}
注意事项
不能重载为成员函数 :成员函数要求左侧操作数必须是当前类的对象,而
>>
的左侧是istream
对象,因此只能用友元函数或普通函数重载。错误处理
:
若输入格式不匹配,需调用
is.setstate(ios::failbit)
设置流的错误状态。可通过
is.clear()
和is.ignore()
清除错误并忽略无效输入。输入格式约定 :需明确告知用户输入格式(如示例中的
(实部, 虚部i)
)。进阶应用
结合自定义格式解析,可实现更复杂的输入功能:
// 支持两种格式: (实部, 虚部i) 或 实部+虚部i
istream& operator>>(istream& is, Complex& c) {
char ch;
double r, i;
if (is >> ch) {
if (ch == '(') { // 格式1: (实部, 虚部i)
if (is >> r >> ch && ch == ',' &&
is >> i >> ch && ch == 'i' &&
is >> ch && ch == ')') {
c.setValue(r, i);
return is;
}
} else { // 回退字符,尝试格式2: 实部+虚部i
is.putback(ch);
if (is >> r >> ch && ch == '+' &&
is >> i >> ch && ch == 'i') {
c.setValue(r, i);
return is;
}
}
}
is.setstate(ios::failbit);
return is;
}
示例:
#include<iostream>
using namespace std;
class Complex
{
public:
friend ostream& operator<<(ostream&, Complex);
friend istream& operator>>(istream&, Complex&);
private:
double real;
double imag;
};
ostream& operator<<(ostream& output, Complex c)
{
output << "(" << c.real << "+" << c.imag << "i)";
return output;
}
istream& operator>>(istream& input, Complex& c)
{
cout << "input real part and imaginary part of complex number:";
input >> c.real >> c.imag;
return input;
}
int main()
{
Complex c1, c2;
cin >> c1;
cout << "c1=" << c1 << endl;
cout << "c2=" << c2 << endl;
return 0;
}
1.8 有关运算符重载的归纳
一、核心价值:运算符重载的意义
类设计的「扩展器」 突破内置类型限制,让自定义类(如复数类、字符串类)能像
int
/double
一样用+
/-
/<<
等运算符,丰富类功能、扩大使用边界 。 例:复数类通过operator+
,直接用c1 + c2
实现复数加法,无需额外写add(c1,c2)
函数。开发效率与可读性 让代码更贴近自然语义,减少专用函数编写。主函数不用为输入输出、运算写大量成员函数,逻辑更简洁,体现 "为用户设计、降低使用成本" 的思想。
二、实践步骤:运算符重载的完整流程
- 规划阶段:明确目标
- 精准定位 :确定要重载的运算符(如
+
/<<
等)与作用类(如复数类、字符串类),1 个重载函数仅对应 "特定运算符 + 特定类" ,不能跨类通用。 例:复数类的+
重载,无法直接用于矩阵类的加法逻辑。
- 设计阶段:函数实现
两种实现形式
:
成员函数:运算符左侧为当前类对象时适用(如
c1 + c2
,c1
作为调用者)。友元 / 普通函数:左侧是
ostream
/istream
(如cout << c
、cin >> c
)等非自定义类对象时,必须用友元(访问私有成员)或普通函数。功能自定义 :函数逻辑完全按需设计,比如让
+
实现复数相加,让<<
按(实部+虚部i)
格式输出。三、技术细节:关键实现要点
- 引用的核心作用
传参优化 :用引用(
&
)作形参,避免拷贝实参 ,减少临时对象创建,提升效率(尤其对象大时明显)。 例:operator+(Complex &c2)
直接操作原对象,而非复制一份。返回值拓展 :返回对象引用时,结果可作为左值(如
(c1 + c2) = c3
,需确保逻辑合理),支持连续操作(如cout << c1 << c2
靠operator<<
返回ostream&
实现)。四、经验拓展:工程化与进阶方向
- 工程化复用
封装沉淀 :将常用运算符重载(如复数类的
+
/-
/<<
/>>
)整理到头文件,集中管理、复用,减少重复开发。用户视角:使用者只需了解头文件中 "重载了哪些运算符、对应哪些类、函数原型" ,就能直接用,降低使用成本。
- 拓展与边界
覆盖范围 :C++ 多数运算符可重载(除
.
/::
/sizeof
等少数),掌握基础后可拓展(如重载=
实现对象赋值、重载>
比较字符串)。风险与积累:自定义重载时,逻辑要清晰(避免与内置语义冲突);特殊需求的重载函数需留存,形成工具库,长期复用。
1.9 不同类型数据间的转换
1.9.1 标准类型数据间的转换
在 C++ 编程中,标准类型(如 int
、double
、char
等)间的数据转换是基础且高频的操作。这些转换由编译器自动处理或通过显式语法触发,理解其规则对写出高效、无歧义的代码至关重要。以下从隐式转换 和显式转换两大维度,拆解 C++ 标准类型转换的核心逻辑。
一、隐式类型转换:编译器自动处理的 "暗箱操作"
隐式转换由编译器在特定场景自动触发,无需开发者显式干预。其设计初衷是简化代码,但也可能因 "自动转换" 隐藏风险,需重点关注。
- 基础类型的自动提升
场景:不同精度的数值类型混合运算时,编译器会自动将 "低精度" 类型转换为 "高精度" 类型,避免精度丢失(但需注意溢出问题)。
示例:
int i = 6;
double d = 7.5;
// 发生隐式转换:i(int)→ double,参与运算后结果为 double
double result = i + d;
规则:
转换方向:
char
/short
→int
→unsigned int
→long
→double
(精度递增方向)。典型场景:算术运算、赋值操作、函数传参(如
int
传给double
形参)。
- 风险:隐式转换的 "副作用"
隐式转换虽便捷,但可能引发精度丢失 或逻辑歧义,需警惕以下场景:
(1)精度丢失
double d = 3.14159;
// 隐式转换:double → int,小数部分被截断
int i = d;
cout << i; // 输出 3,丢失小数精度
(2)逻辑歧义
bool flag = true;
// 隐式转换:bool → int,true → 1,false → 0
int num = flag;
cout << num; // 输出 1,逻辑上可能与业务预期冲突
二、显式类型转换:主动控制转换逻辑
为避免隐式转换的风险,C++ 提供显式转换语法,让开发者主动控制类型转换的时机和规则。
- C++ 推荐语法:
static_cast
(安全显式转换)语法 :
static_cast<目标类型>(源数据)
适用场景:基础类型转换、父类与子类指针转换(需确保安全)。示例:
double d = 3.14;
// 显式转换:double → int,主动控制精度丢失
int i = static_cast<int>(d);
优势:
编译期检查合法性,避免危险转换(如
int
↔指针
直接转换)。语义清晰,明确告知读者 "此处是主动类型转换"。
- 兼容 C 语法:
(类型名)数据
(谨慎使用)语法 :
(目标类型)源数据
风险:
编译器不会检查转换合法性,可能引发未定义行为(如
int
强制转指针
)。语义模糊,维护性差,仅建议在纯 C 代码或极特殊场景使用。
示例(危险用法):
int num = 10;
// 危险:int → 指针,可能访问非法内存
char* ptr = (char*)num;
三、场景化实践:避坑与最佳实践
- 算术运算:优先统一类型再计算
int a = 5;
double b = 2.0;
// 主动显式转换,避免隐式转换的"暗箱操作"
double result = static_cast<double>(a) / b;
- 函数传参:匹配形参类型
void printDouble(double d) { /* ... */ }
int main() {
int num = 10;
// 显式转换:int → double,清晰传递意图
printDouble(static_cast<double>(num));
}
- 避免 "二义性" 转换
若类同时定义了转换构造函数 和类型转换函数,需手动明确转换逻辑,避免编译器歧义:
class Complex {
public:
// 转换构造函数:double → Complex
Complex(double d) : real(d), imag(0) {}
// 类型转换函数:Complex → double
operator double() { return real; }
private:
double real, imag;
};
int main() {
Complex c(3.5);
double d = 2.5;
// 显式明确:c → double 后参与运算
double result = d + static_cast<double>(c);
}
四、总结:掌握转换规则,写出清晰、安全的代码
C++ 标准类型转换的核心是平衡 "便捷性" 与 "安全性":
隐式转换:简化基础场景代码,但需警惕精度丢失和逻辑歧义。
显式转换 :通过
static_cast
主动控制转换,提升代码可读性与安全性。在实际开发中,建议优先使用显式转换 (尤其是涉及复杂逻辑或类对象时),让类型转换的意图清晰可见,减少调试成本。同时,结合编译器警告(如
-Wconversion
),可进一步规避隐式转换带来的潜在风险。
1.9.2 把其他类型数据转换为类对象------用转换构造函数
在 C++ 面向对象编程里,当需要将外部数据(如基础类型、其他类对象)转化为自定义类对象时,转换构造函数 是关键工具。它能让类灵活适配外部输入,让对象创建更自然,以下从核心概念、实现逻辑、场景拓展全方位拆解。
一、核心定义:什么是转换构造函数
转换构造函数是特殊的构造函数 ,特点是仅接收 1 个参数 ,作用是把该参数(其他类型数据)"包装" 成当前类的对象。它本质是构造函数的重载形式,让类能兼容外部数据类型。
语法示例(以
Complex
类为例,实现double
→Complex
转换 ):class Complex {
public:
// 转换构造函数:用 double 构造 Complex,虚部默认 0
Complex(double realPart) : real(realPart), imag(0) {}
private:
double real, imag;
};
二、实现逻辑:如何让外部数据 "变成" 类对象
- 触发条件
当代码中用 "非当前类类型" 的数据创建对象时,编译器自动调用转换构造函数。
场景示例:
// 触发转换构造函数:double(3.5) → Complex
Complex c(3.5);
- 函数规则
参数限制 :只能有 1 个参数 (需转换的目标类型,如
double
、其他类对象 )。重载特性 :可与默认构造函数、普通构造函数共存,编译器按实参类型 / 数量匹配。
代码对比(
Complex
类的多构造函数重载 ):class Complex {
public:
// 默认构造函数(无参)
Complex() : real(0), imag(0) {}
// 转换构造函数(double → Complex)
Complex(double r) : real(r), imag(0) {}
// 普通构造函数(双参,初始化实部+虚部)
Complex(double r, double i) : real(r), imag(i) {}
private:
double real, imag;
};
调用逻辑:
Complex c1; // 调用默认构造函数
Complex c2(2.5); // 调用转换构造函数(double → Complex)
Complex c3(1.2, 3.4); // 调用普通双参构造函数
三、场景拓展:不止基础类型,类对象也能转
转换构造函数不仅支持 基础类型(如
double
/int
) 转类对象,还能实现 "其他类对象" 转当前类对象,让类间转换更灵活。
- 跨类转换示例(
Student
→Teacher
)class Student {
public:
string name;
int age;
Student(string n, int a) : name(n), age(a) {}
};
class Teacher {
public:
string name;
int age;
// 转换构造函数:Student → Teacher
Teacher(const Student& s) : name(s.name), age(s.age) {}
};
int main() {
Student s("Alice", 20);
// 触发转换构造函数:Student → Teacher
Teacher t(s);
return 0;
}
- 注意事项:权限与合法性
成员访问 :若转换涉及其他类的私有成员,需通过 友元函数 或 公共接口 实现(如
Student
的name
/age
需是public
,或Teacher
声明为Student
的友元 )。逻辑合理 :转换需符合业务逻辑(如
Student
转Teacher
,需确保数据映射有意义 )。四、与其他构造函数的区别
构造函数类型 核心特点 典型场景 默认构造函数 无参数,初始化默认状态 Complex c;
创建空复数普通构造函数 多参数,灵活初始化成员 Complex c(1.2, 3.4);
完整赋值转换构造函数 单参数,适配外部类型转换 Complex c(2.5);
用 double 转复数复制构造函数 参数为 "本类对象引用",拷贝对象 Complex c2(c1);
复制对象五、实战价值:让类更易用的关键设计
转换构造函数的核心价值是降低类的使用门槛:
对用户:无需手动写复杂转换逻辑,像用基础类型一样创建类对象(如
Complex c(3.5);
直观又简洁 )。对设计:让类能无缝对接外部系统(如输入输出、其他模块数据),拓展类的适用场景。
总结:转换构造函数的本质是 "类的兼容性桥梁"
它让自定义类突破自身类型限制,能直接用外部数据(基础类型、其他类对象)创建对象,是 C++ 面向对象设计中 "灵活适配外部输入" 的核心手段。掌握它,就能让类的交互更自然,代码更简洁,尤其在处理复杂数据流转场景(如数学库、业务对象转换)时,能大幅提升开发效率与代码可读性。
1.9.3 将类对象转换为其他数据类型------用类型转换函数
在 C++ 编程中,当需要将自定义类对象转换为基础类型(如 int
、double
)或其他类对象时,类型转换函数 是核心工具。它能让类对象 "变身" 为其他类型,无缝融入外部系统。以下从概念、实现、应用场景全面解析。
一、核心定义:什么是类型转换函数
类型转换函数是类的特殊成员函数 ,作用是将当前类对象转换为其他类型。其语法为:
operator 目标类型() const {
// 返回与目标类型兼容的值
}
关键点:
无返回类型声明 :函数名前直接用
operator 目标类型
,无需显式写return type
。无参数:不接受任何参数,因为转换逻辑基于当前对象自身数据。
常函数 :通常用
const
修饰(避免修改对象状态)。二、实现逻辑:如何让类对象 "变身"
- 基础类型转换示例(
Complex
→double
)class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 类型转换函数:Complex → double(返回模长)
operator double() const {
return sqrt(real * real + imag * imag);
}
};
// 使用示例
Complex c(3, 4);
double d = c; // 自动转换:调用 operator double(),d = 5.0
- 转换为其他类对象(
Date
→string
)class Date {
private:
int year, month, day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
// 类型转换函数:Date → string
operator string() const {
return to_string(year) + "-" + to_string(month) + "-" + to_string(day);
}
};
// 使用示例
Date date(2023, 10, 1);
string s = date; // 自动转换:s = "2023-10-1"
三、触发机制:何时自动调用类型转换函数
当类对象出现在以下场景时,编译器会自动调用匹配的类型转换函数:
赋值操作 :将对象赋值给目标类型变量(如
double d = c;
)。函数传参:对象作为参数传递给期望目标类型的函数。
表达式运算 :对象参与目标类型的运算(如
c + 3.14
,若c
可转为double
)。四、与转换构造函数的对比
特性 类型转换函数(类 → 其他) 转换构造函数(其他 → 类) 函数形式 operator 目标类型()
类名(目标类型 参数)
所属 类的成员函数 类的构造函数 触发场景 对象出现在目标类型上下文中 用目标类型数据创建对象 典型用途 对象转基础类型 / 其他类 基础类型 / 其他类转对象 五、注意事项:避免转换歧义
- 双向转换的冲突
若类同时定义了转换构造函数 和类型转换函数,可能导致二义性。例如:
class Complex {
public:
// 转换构造函数:double → Complex
Complex(double d) : real(d), imag(0) {}
// 类型转换函数:Complex → double
operator double() const { return real; }
private:
double real, imag;
};
// 二义性场景
Complex c(3.5);
double result = 2.5 + c; // 歧义:
// 1. c → double(调用 operator double())
// 2. 2.5 → Complex(调用转换构造函数)
解决方法:
优先显式转换 :用
static_cast<double>(c)
明确转换方向。避免双向转换:除非必要,不同时定义冲突的转换函数。
- 慎用隐式转换
过度使用类型转换函数可能导致代码可读性下降(如对象在不知不觉中被转换)。建议:
用显式转换函数 :通过普通成员函数(如
toDouble()
)替代隐式类型转换。用
explicit
关键字 :对转换构造函数添加explicit
,强制显式转换。六、实战场景:提升类的灵活性
- 数值类适配基础运算
让
Fraction
类(分数)直接参与double
运算:class Fraction {
private:
int numerator, denominator;
public:
// 类型转换函数:Fraction → double
operator double() const {
return static_cast<double>(numerator) / denominator;
}
};
// 使用示例
Fraction f(3, 4); // 3/4
double result = f + 0.5; // 自动转换:0.75 + 0.5 = 1.25
- 自定义类与标准库集成
让
MyVector
类对象可直接赋值给std::vector
:class MyVector {
private:
std::vector<int> data;
public:
// 类型转换函数:MyVector → std::vector<int>
operator std::vector<int>() const {
return data;
}
};
// 使用示例
MyVector mv;
std::vector<int> stdVec = mv; // 自动转换
总结:类型转换函数是类的 "外向接口"
它让自定义类不再 "闭门造车",能主动与外部类型系统交互,是实现类对象与其他数据类型无缝对接的关键工具。合理使用类型转换函数(结合显式转换避免歧义),能显著提升类的灵活性与复用性,让代码更符合自然语义。
2、函数重载
函数重载(Function Overloading)是 C++ 的一项重要特性,它允许在同一作用域内定义多个同名函数,但这些函数的参数列表必须不同。通过函数重载,程序员可以用同一个函数名处理不同类型或数量的参数,提高代码的可读性和灵活性。
2.1 核心概念
-
相同作用域:重载函数必须在同一个类、命名空间或全局作用域中。
-
参数列表不同
:
-
参数类型不同(如
int
vsdouble
)。 -
参数数量不同(如 2 个参数 vs 3 个参数)。
-
参数顺序不同(如
(int, double)
vs(double, int)
)。
-
-
返回类型无关:仅返回类型不同不足以构成重载,会导致编译错误。
2.2函数重载的实现机制
C++ 通过 **名称修饰(Name Mangling)**实现函数重载。编译器会根据函数名、参数类型和数量生成唯一的内部标识符。例如:
int add(int a, int b); // 内部可能被命名为 _Z3addii
double add(double a, double b); // 内部可能被命名为 _Z3adddd
这种机制确保编译器能区分同名但参数不同的函数。
示例:基础函数重载
#include <iostream>
using namespace std;
// 计算整数和
int sum(int a, int b) {
return a + b;
}
// 计算三个整数的和
int sum(int a, int b, int c) {
return a + b + c;
}
// 计算双精度浮点数的和
double sum(double a, double b) {
return a + b;
}
int main() {
cout << sum(1, 2) << endl; // 调用 sum(int, int)
cout << sum(1, 2, 3) << endl; // 调用 sum(int, int, int)
cout << sum(1.5, 2.5) << endl; // 调用 sum(double, double)
return 0;
}
2.3 参数类型差异
重载函数的参数类型可以是不同的基本类型、自定义类型或指针:
// 基本类型差异
void print(int num) {
cout << "Integer: " << num << endl;
}
void print(double num) {
cout << "Double: " << num << endl;
}
// 自定义类型差异
class Point {
int x, y;
};
void process(Point p) { /* ... */ }
void process(Point* p) { /* ... */ } // 重载指针类型
2.4 参数数量差异
通过参数数量重载函数是常见的做法:
// 带默认参数的函数(本质上也是参数数量不同)
void draw(int x, int y) {
cout << "Drawing at (" << x << ", " << y << ")" << endl;
}
void draw(int x, int y, int color) {
cout << "Drawing at (" << x << ", " << y << ") with color " << color << endl;
}
// 也可以通过默认参数实现类似效果
void draw2(int x, int y, int color = 0) {
cout << "Drawing2 at (" << x << ", " << y << ") with color " << color << endl;
}
2.5参数顺序差异
参数类型相同但顺序不同也可以构成重载:
void combine(int a, double b) {
cout << "int then double: " << a + b << endl;
}
void combine(double a, int b) {
cout << "double then int: " << a + b << endl;
}
// 调用示例
combine(1, 2.5); // 输出 "int then double: 3.5"
combine(1.5, 2); // 输出 "double then int: 3.5"
2.6 函数重载与模板
函数模板(Function Template)可以实现类似的功能,但二者有区别:
-
函数重载:为不同类型显式定义多个函数。
-
函数模板:通过泛型自动生成函数。
// 函数模板示例
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 重载函数模板
template <typename T>
T max(T a, T b, T c) {
return max(max(a, b), c);
}
2.7 重载决议(Overload Resolution)
当调用重载函数时,编译器通过以下步骤选择最合适的版本:
-
精确匹配:优先选择无需类型转换的函数。
-
隐式转换匹配 :尝试通过隐式转换(如
int
→double
)找到匹配函数。 -
模板实例化:如果存在匹配的函数模板,将其实例化。
-
报错:若存在多个可行的匹配且无法确定最佳选择,则引发歧义错误。
示例:
void f(int); // #1
void f(double); // #2
void f(char); // #3
f('a'); // 精确匹配 #3
f(5); // 精确匹配 #1
f(3.14); // 精确匹配 #2
f(5L); // 长整型转换为int,调用 #1
2.8 常见问题与注意事项
-
返回类型不能单独作为重载依据:
int func(); // 错误:与下面的函数仅返回类型不同 double func(); // 编译错误
-
默认参数可能导致歧义:
void f(int a, int b = 0); void f(int a); // 错误:与上面的函数冲突
-
引用与常量引用的重载:
void g(int& x); // 匹配可修改的左值 void g(const int& x); // 匹配常量左值和右值
-
函数指针与重载:
void h(int); void h(double); void (*ptr)(int) = h; // 正确:ptr指向h(int) void (*ptr2)(double) = h; // 正确:ptr2指向h(double)
总结
函数重载是 C++ 中提高代码可读性和灵活性的重要手段,它允许使用同一函数名处理不同类型的参数。通过合理设计参数列表(类型、数量或顺序),可以为不同场景提供统一的接口,同时保持类型安全。但需注意避免重载导致的歧义,确保代码清晰易懂。