创建引用变量
C++和C都用&符号来表示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。例如:
int rats
int & rodents=rats // make rodents an alias for rats(给rats取一个别名)
&不是地址操作符,而是类型标识符的一部分,将rodents的类型声明为int &,即int变量的引用。就像声明中的char* 指的是指向char的指针一样, int &指的是指向int的引用。下述引用声明允许将rats和rodents互换——它们指向相同的值和内存单元。
rats的地址以及所占内存的大小完全相同,且对其中一个进行操作,另一变量也会有相同操作。
rats=100
cout<<rats<<rodents;
rats++
cout<<rats<<rodents;
第一个输出结果都为100,第二个输出结果为101.
要与C语言中的指针区别开。C语言中的指针声明形式和引用类似,但是仍有不同。
指针的声明可以有两种:
int * p=&rats;//初始化赋予地址
int * p;//非初始化赋值
p=&a;
而引用的声明一定要初始化
int & p=rodents;//初始化
int & rodent;
rodent=rat;//不允许这样做
引用更接运const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。
将函数作为引用参数
C++与C语言相比,不仅能实现按值传递和按指针传递,也能实现按引用传递,即引用作为函数参数,使得函数中的变量名成为调用程序中的变量的别名。
- 按值传递和按引用传递
如图,按值传递需要创建两个变量(使用两块内存),按值传递是将实参的值赋值给形参。按引用传递,是使x成为实参的别名,x指向times变量并没有再申请空间。从这里就看出,引用传递效率更高且节省内存。
- 引用与使用指针作比较
我们用一个经常用的例子来进行对比,即实现两值的交换。
#include<iostream>
void swapr(int & a,int & b){ //使用引用进行值交换
int temp;
temp=a;
a=b;
b=temp;
}
void swapp(int*a,int*b){ //使用指针进行值交换
int temp;
temp=*a;
*a=*b;
*b=temp;
}
void swapv(int a,int b){ //按值传递
int temp;
temp=a;
a=b;
b=temp;
}
int main()
{
using namespace std;
int wallet1 =100;
int wallet2 =350;
cout <<"wallet1 :" << wallet1;
cout << "wallet2 :" << wallet2 <<endl;
cout <<"Using references to swap contents: \n";
swapr(wallet1,wallet2); // pass variables
cout <<"wallet1 :" << wallet1;
cout << "wallet2 :" << wallet2 <<endl;
cout << "Using pointers to swap contents: \n";
swapp (&wallet1, &wallet2); // pass addresses of variables
cout <<"wallet1 :" << wallet1;
cout << "wallet2 :" << wallet2 <<endl;
cout << "Using value to swap contents \n";
swapv (wallet1, wallet2); // pass addresses of variables
cout <<"wallet1 :" << wallet1;
cout << "wallet2 :" << wallet2 <<endl;
return 0;
}
结果:
由结果我们可以知道,按值传递交换失败,按指针传递交换成功,使用引用值也成功交换。下面我们分析原因。
按值传递的形式,是把实参的值复制给了形参,所以形参的变化并不会影响到原来的值。
指针传递,是将原来实参的地址传递给了形参,指针通过地址找到了原来实参的值,所以值发生改变。
引用形式,我们知道形参其实是实参的另一种变量名(形参实质指向的实参,也就是表示的是同一个内存的变量),所以形参的值发生变化,实参的值也发生变化。
- 引用的属性和特别之处
先看例子:
#include<iostream>
double cube(double x){
x*=x*x;
return x;
}
double recube( double &y){
y*=y*y;
return y;
}
int main()
{
using namespace std;
double a =3.0;
cout<<"cube is:"<<cube(a)<<endl;
cout<<" value is"<<a<<endl;
cout<<"recube is:"<<recube(a)<<endl;
cout<<" value is"<<a<<endl;
return 0;
}
第一个按值传递,实参a的值并没有发生变化。第二个实参a的值发生了变化,因为使用的是引用。
所以应选取合适的方式来使用变量,我们有时候设计程序并不需要改变原来实参的值,这时就应该采用按值传递的形式。
将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而 不是基本的内置类型。
下面通过实例来看一下结构的应用
#include <iostream>
#include<string>
struct free_throws{
std::string name;
int made;
int attempts;
float percent;
};
void display(const free_throws & ft){ //定义结构引用,使用const使得结构不可变
using namespace std;
cout<<"name:"<<ft.name<<endl;
cout<<"made:"<<ft.made<<'\t';
cout<<"attempts:"<<ft.attempts<<'\t';
cout<<"percent:"<<ft.percent<<endl;
}
void set_pc(free_throws & ft){
if(ft.attempts!=0)
ft.percent=100.0f*float(ft.made)/float(ft.attempts);
else
ft.percent=0;
}
free_throws & accumulate(free_throws & target, const free_throws & source){//结构的两个引用作为参数,返回第一个结构的引用
target.attempts+=source.attempts;
target.made+=source.made;
set_pc(target);
return target;
}
int main()
{
using namespace std;
free_throws one{"xuxu",13,14}; //初始化
free_throws two{"lalla",10,16};
free_throws three{"tttt",7,9};
free_throws four{"ssss",5,9};
free_throws team{"ffff",0,0};
set_pc(one); //将结构作为参数
display(one);
accumulate(team,one);
display(team);
display(accumulate(team,two));
accumulate(accumulate(team,three),four);
display(team);
return 0;
}
-
结果
-
程序分析
(1)void display(const free_throws & ft),display函数只显示内容而不修改所以使用const。
(2)free_throws & accumulate(free_throws & target, const free_throws & source):该函数定义形势是返回一个引用,程序返回的是参数里第一个可以更改的应用。accumlate(team,one),targe指向team,函数accumulate修改team后并返回–>return target;
(3)accumulate(accumulate(team,three),four):该调用相当于
accumulate(accumulate(team,three);
accumulate(accumulate(team,four);
因此,由程序结果可知,最终的team中made值和attempts值是one,two,three,累加后的结果。所以我们可以看到,结构中应用引用,可以不用传递结构中整个变量,从而提高运行速度。一定情况下能够完后相当于指针的作用,能够通过形参来改变实参的值。
引用应用于类对象
例1:
#include<iostream>
class Time{
private:
int hours;
int minutes;
public:
Time();
Time (int h, int m = 0);
void Reset (int h=0,int m = 0);
Time Sum (const Time & t) const;
void Show()const;
};
Time::Time(int h,int m){
hours=h;
minutes=m;
}
Time::Time(){
hours=minutes=0;
}
void Time::Reset(int h, int m){
hours=h;
minutes=m;
}
Time Time::Sum(const Time & t) const{ //对象引用
Time sum;//创建一个新的对象
sum.minutes =minutes + t.minutes;//新对象的分等于coding对象的minutes+引用对象t的分
sum.hours =hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;//返回sum对象
}
void Time::Show()const{
std::cout << hours << "hours " << minutes <<"minutes";
}
int main(){
using namespace std;
Time planning;
Time coding(2, 40);
Time fixing(5, 55);
Time total;
cout << "planning time = ";
planning.Show();
cout <<endl;
cout << "coding time=";
coding.Show();
cout<<endl;
cout <<"fixing time = ";
fixing.Show();
cout << endl;
total = coding.Sum(fixing); //实际是两个对象,一个是coding对象,一个是作为引用实参的对象fixing
cout <<"coding.Sum (fixing)=";
total.Show();
cout <<endl;
return 0;
}
结果:
注意参数是引用,但返回类型却不是引用。将参数声明为引用的目的是为了提高效率。如果按值传递Time对象,代码的功能将相同(改为Time Time::Sum(const Time t) const即可),但传递引用,速度将更快,使用的内存将更少(原因前面已论述过)。不过,返冋值不能是引用。因为函数数将创建一个新的Time对象(sum),来表示另外两个Time对象的 和。返冋对象(如代码所做的那样)将创建对象的拷贝,而调用函数可以使用它。不过,如果返回类型为 Time&,则引用的将是sum对象。但由于sum对象是局部变量,在函数结束时将彼删除,因此引用将指向 一个不存在的对象。然而,使用Time返回类型意味着程序将在删除sum之前构造它的拷贝,调用函数将 得到该拷贝。
引用应用于运算符重载
我们继续上面的程序,应用于一个简单的运算符重载,什么是运算符重载我们接下来会讲,先引入一个小例子说明:
#include<iostream>
class Time{
private:
int hours;
int minutes;
public:
Time();
Time (int h, int m = 0);
void Reset (int h=0,int m = 0);
Time operator+(const Time & t) const;
void Show()const;
};
Time::Time(int h,int m){
hours=h;
minutes=m;
}
Time::Time(){
hours=minutes=0;
}
void Time::Reset(int h, int m){
hours=h;
minutes=m;
}
Time Time::operator+(const Time & t) const{ //对象引用
Time sum;//创建一个新的对象
sum.minutes =minutes + t.minutes;//新对象的分等于coding对象的minutes+引用对象t的分
sum.hours =hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;//返回sum对象
}
void Time::Show()const{
std::cout << hours << "hours " << minutes <<"minutes";
}
int main(){
using namespace std;
Time planning;
Time coding(2, 40);
Time fixing(5, 55);
Time total1;
Time total2;
cout << "planning time = ";
planning.Show();
cout <<endl;
cout << "coding time=";
coding.Show();
cout<<endl;
cout <<"fixing time = ";
fixing.Show();
cout << endl;
total1 = coding.operator+(fixing); //实际是两个对象,一个是coding对象,一个是作为引用实参的对象fixing
cout <<"coding.Sum (fixing)=";
total1.Show();
cout <<endl;
total2 = coding+fixing; //验证 total=coding.operator+(fixing)和 total2 = coding+fixing结果是否一样
cout <<"coding.Sum (fixing)=";
total2.Show();
cout <<endl;
return 0;
}
结果:
分析:
将Time类转换为重载的加法操作符很容易,只要将Sum()的名称改为operator + ()即可。
和Sum() 样,operator+()也是由Time对象调用的,它将第二个Time对象作为参数,并返回一个 Time对象。因此,可以像调用Sum()那样来调用operator+()方法:
total=coding.operator+(fixing); // function notation
不过将该方法命令为operator+ ()后,也可以使用操作符表示法: total =coding + fixing; // operator notation
这两种农示法都将调用operator+ ()方法。注意,在操作符表示法中,操作符左侧的对象(这里为coding) 是调用对象,操作符右边的对象(这里为fixing)是作为参数被传递的对象。
何时使用引用参数
- 使用引用参数的原因:
(1)程序员能够修改调用函数中的数据对象(也就是能根据函数的形参而改变实参的值,类似于指针的作用)。
(2)通过传递引用而不是整个数据对象,可以提髙程序的运行速度。 - 各类型的使用原则:
对于使用传递的值而不作修改的函数:
(1)如果数据对象很小,如内置数据类型或小型结构,则按值传递。
(2)如果数据对象是数组,则使用指针,因为这是惟一的选择,并将指针声明为指向const的指针。
(3)如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
(4)如果数据对象是类对象.则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的上要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
(1)如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit (&x)这样的代码(其中x是int 型),则很明显.该函数将修改。
(2)如果数据对象是数组,则只能使用指针。
(3)如果数据对象是结构,则使用引用或指针。
(4)如果数据对象是类对象,则使用引用。
当然,这些都只是一些基本原则,具体的使用还是要根据自己的要求和使用场景。
为什么引入运算符重载?
这个问题看似简单,但确实折磨了自己很久没有想明白,原因是和成员函数混淆了。
我们先来看一下咱这个错误程序,以这个程序来解释我们为什么引入运算符重载。
#include <iostream>
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
public:
int m_A;
int m_B;
};
void test() {
using namespace std;
Person p1(10, 10);
Person p2(20, 20);
Person p3 = p2 + p1;
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
int main() {
using namespace std;
test();
system("pause");
return 0;
}
代码来源:C++核心编程–运算符重载 - Zhuosd的文章 - 知乎 https://zhuanlan.zhihu.com/p/122893724
- 假设我们如果用以上程序来进行两个对象的相加并赋值给第三个对象。想法很好,但是让我们看下结果:
,上面的大概意思就是没有相匹配的operater+()运算形式,这个运算形式就是运算符重载。这个我们稍后再详细讲解。
- 那如果不使用运算符重载,我们应该如何使对象的数据相加?
我们改进一下:
#include <iostream>
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
public:
int m_A;
int m_B;
};
void test() {
using namespace std;
Person p1(10, 10);
Person p2(20, 20);
Person p3(0,0);
Person p4(0,0);
p3.m_A= p2.m_A + p1.m_A;
p3.m_B=p2.m_B+p1.m_B;
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
p4.m_A= p3.m_A + 10; //相当于 operator+(p3,10)
p4.m_B= p3.m_B+ 10;
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
int main() {
using namespace std;
test();
system("pause");
return 0;
}
从一上程序我们可以看到,在对象进行运算时我们必须以对象名.类成员的形式对对象进行运算,这样看着似乎冗余且不美观。而运算符重载正是解决了这一问题。
- C++允许将操作符重载扩展到用户定义的类型,例如,允许使用+将两个对象相加。编译器将根据操作 数的数目和类型决定使用哪种加法定义。重载操作符可使代码看起来更自然。
例如,实现p3.m_A= p2.m_A + p1.m_A;我们可以通过重新定义加法运算符来实现直接将对象相加:p3=p2+p1.
重载运算符的定义和使用
- 定义方式:
<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
- 我们接着使用重载运算符来改进我们的实例:
#include<iostream>
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person & p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
public:
int m_A;
int m_B;
};
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
void test() {
using namespace std;
Person p1(10, 10);
Person p2(20, 20);
Person p3 = p2 + p1; //相当于 p2.operaor+(p1),对对象直接操作
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
int main() {
test();
system("pause");
return 0;
}
重载限制
重载规则:
(1)为了防止用户对标准类型进行运算符重载,C++规定重载后的运算符的操作对象必须至少有一个是用户
定义的类型。
比如说现在有两个数:int number1,int number2,那么number1+number2 求的是两个数的和,但是如果你重载以后让着两个数相加为他们的乘积,这肯定是不合乎逻辑的。可能重载以后会有二义性,导致程序不知道该执行哪一个(是自带的的还是重载后的函数)
(2)使用运算符不能违法运算符原来的句法规则。如不能将% 重载为一个操作数,
例
int x:
Time shiva:
% x: // invalid for modulus operator
% shiva: // invalid for overloaded operator
(3)不能修改运算符原先的优先级。
(4)不能创建一个新的运算符,例如不能定义operator** (···)来表示求幂
(5)不能进行重载的运算符:成员运算符,作用域运算符,条件运算符,sizeof运算符,typeid(一个RTTI运算符),const_cast、dynamic_cast、reinterpret_cast、static_cast强制类型转换运算符
(6)大多数运算符可以通过成员函数和非成员函数进行重载但是下面这四种运算符只能通过成函数进行重载:= 赋值运算符,()函数调用运算符,[ ]下标运算符,->通过指针访问类成员的运算符。
(7)除了上述的规则,其实我们还应该注意在重载运算符的时候遵守一些明智的规则:例如:不要将+运算符重载为交换两个对象的值。
(8)可被重载的操作符如下图:
注意,这些运算符只能通过成员函数进行重载:
= 赋值运算符,
()函数调用运算符,
[ ]下标运算符,
->通过指针访问类成员的运算符。
其他运算符实例
我们已经看过加法实例,我们再来看一个减法乘法实例:
代码来源:https://blog.csdn.net/lishuzhai/article/details/50781753?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160618480819725225046765%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=160618480819725225046765&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogtop_click~default-1-50781753.pc_v1_rank_blog_v1&utm_term=c%2B%2B%E8%BF%90%E7%AE%97%E7%AC%A6%E9%87%8D%E8%BD%BD&spm=1018.2118.3001.4450
头文件:
MyTime.h文件:
#pragma once
#ifndef MYTIME_H_
#define MYTIME_H_
class CMyTime
{
private:
int m_hours;
int m_minutes;
public:
CMyTime();
CMyTime(int h, int m = 0);
void AddHr(int h); //小时更改
void AddMin(int m);//分钟更改
void Reset(int h = 0, int m = 0); //重新设置时间
CMyTime operator+(const CMyTime &t) const; //重载加法
CMyTime operator-(const CMyTime &t) const; //重载减法
CMyTime operator*(double n) const; //重载乘法
void Show() const;
~CMyTime();
};
#endif
cpp文件:
#include "MyTime.h"
#include <iostream>
CMyTime::CMyTime()
{
m_hours = 0;
m_minutes = 0;
}
CMyTime::CMyTime(int h, int m)
{
m_hours = h;
m_minutes = m;
}
CMyTime::~CMyTime()
{
}
void CMyTime::AddHr(int h) //小时更改
{
m_hours += h;
}
void CMyTime::AddMin(int m) //分钟更改
{
m_minutes = m;
}
void CMyTime::Reset(int h, int m) //重新设置时间
{
m_hours = h;
m_minutes = m;
}
CMyTime CMyTime::operator+(const CMyTime &t) const //重载加法运算符函数
{
CMyTime sum;
sum.m_minutes = t.m_minutes + m_minutes;
sum.m_hours = t.m_hours + m_hours + sum.m_minutes / 60;
sum.m_minutes %= 60;
return sum;
}
CMyTime CMyTime::operator-(const CMyTime &t) const //重载为减法运算符函数
{
CMyTime diff;
int tot1, tot2;
tot1 = t.m_minutes + 60 * t.m_hours;
tot2 = m_minutes + 60 * m_hours;
diff.m_minutes = (tot2 - tot1) % 60;
diff.m_hours = (tot2 - tot1) / 60;
return diff;
}
CMyTime CMyTime::operator*(double n) const //重载为乘法运算符函数。
{
CMyTime result;
long totalMinutes = m_hours * 60 * n+ m_minutes *n;
result.m_minutes = totalMinutes % 60;
result.m_hours = totalMinutes / 60;
return result;
}
void CMyTime::Show() const
{
std::cout << m_hours << " hours "
<< m_minutes << " minutes\n";
}
主函数:
int main()
{
using namespace std;
using std::cout;
using std::endl;
CMyTime weeding(4, 35);
CMyTime waxing(2, 47);
CMyTime total;
CMyTime diff;
CMyTime adjusted;
cout << "weeding Time = ";
weeding.Show();
cout << endl;
cout << "waxing Time = ";
waxing.Show();
cout << endl;
cout << "total work Time = "; /
total = weeding + waxing;
total.Show();
cout << endl;
diff = weeding - waxing;
cout << "weeding Time - waxing Time = ";
diff.Show();
cout << endl;
adjusted = total *1.5; //(3)
cout << "adjusted work Time = ";
adjusted.Show();
cout << endl;
return 0;
}
结果:
重载<<运算符
这个单独列出来一栏是因为该运算符比较常用,需要掌握。
- 假设我们有这样一个类成员函数来显示时间
void time::show(){
std::cout<<hours<<"hours,"<<minutes<<"minutes";
}
那么我们在主函数创建对象,如果显示对象的时间就绪要这样做:
Time time;
time.show();
接下来,我们如果想在main函数里直接打印对象的时间那么该如何做?这时候就要用到运算符重载<<
来实现:
cout<<time;//利用运算符重载直接显示对象时间
- <<第一种重载版本
要使Time类知道使用cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句:
cout <<trip;
可以看到需要使用两个对象,一个是ostream类对象,另一个是time对象来重载<<,我们知道,
使用两个对象,如果使用一个Time成员函数来重载<<, Time对象将作为第一个操作数(classneme.opeator<<),而cout作为第二个操作数。这意味着必须这样使用<<:
trip <<cout: // if operator«()were a Time member function
通过使用友元函数,可以像下面这样重载运算符
void operat<<(ostream & os,const Time & t) { //一个是cout引用(ostream类),一个是time对象引用(TIME类)
os << t.hours<< ” hours."<< t.minutes « “minutes.";
}
这样可以使用下面的语句:
cout<< trip;
- <<的第二种重载版本
前面介绍的实现存在一个问题.像下面这样的语句:
cout <<trip;
可以正常工作,但将重新定义的<<操作符与cout—起使用时,不允许将输出拼接起来,而通常是可以拼接的:
cout << "Trip time"<< trip<< " (Tuesday) \n"; // can't do
为了更好的理解如何实现这种操作,我们举个例子:
cout<<x<<y;
先看cout<<x,ostream类中,<<操作符左边要求是cout对象,所以coutx可以实现,那么要实现(cout<<x)<<y也可以正常工作,就要求(cout<<x)也作为一个cout对象。
所以对于友元函数,我们只要修改operator<<()函数,让它返回ostream对象的引用即可:
ostream & operator<< (ostream & os, const Time & t)
{
os << t.hours << "hours"<< t.minutes <<"minutes";
return os; //返回ostream对象的引用(对象定义ostream &)
}
- 例:
#include<iostream>
#include<cstring>
class String
{
private:
char *str;
int len;
public:
String(const char *s); //comstructor
~String(); //destructor
friend std::ostream & operator<<(std::ostream & os, const String & st);
};
String::String(const char * s)
{
using namespace std;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
}
String::~String()
{
using namespace std;
cout << "\"" << str << "\" object deleted, "<<endl;
delete[] str;
}
std::ostream & operator<<(std::ostream & os, const String & st){
os<<st.str;
return os;
}
int main(){
using namespace std;
String headline1("hello,everyone!");
String headline2("my name is jack");
cout<<"headline1:"<<headline1<<endl;
cout<<"headline2"<<headline2<<endl;
return 0;
}
结果:
从结果可以看到,由std::ostream & operator<<(std::ostream & os, const String & st)进行运算符重载后可以直接打印对象。
那么我们将这段代码注释掉后看看会有什么样的结果
std::ostream & operator<<(std::ostream & os, const String & st){
os<<st.str;
return os;
}
那么就出现需要定义重载运算符<<的字样,说明使用cout不能直接作用于对象需要使用重载运算符<<.