重載運算符
問題引入
int a=10
int b=20
int c=a+b
對於內置數據類型編譯器知道如何運算(+)
然後對於自定義類型
People p1+People p2 編譯器是處理不了的,因爲People類內部沒有定義 操作符 +,
在People類內定義成員函數
#include <iostream>
#include<string>
using namespace std;
class People {
public:
string name;
int age;
People(string n, int a) :name(n), age(a) {};
People& plus(const People& p) {
this->name = p.name + this->name;
this->age = p.age + this->age;
return *this;
}
};
void test() {
People p1("a",19);
People p2("b", 11);
People p3=p1.plus(p2);
cout<<p3.name<<endl;
cout << p3.age << endl;
}
int main() {
test();
return EXIT_SUCCESS;
}
結果
ba
30
People p1.plus(p2)
People p1.operator+(p2)
把成員函數名字從plus換成operator+,然後調用的時候可以直用 p1+p2,operator省略了
operator=運算符的重載
#include<iostream>
#include<string>
using namespace::std;
class MyStr {
private:
string name;
int id;
public:
MyStr() {
cout << "默認構造" << endl;
}
MyStr(int id, string name) {
cout << "有參構造" << endl;
this->id = id;
this->name =name;
}
MyStr(const MyStr& str) {
cout << "拷貝構造" << endl;
this->id = str.id;
this->name = str.name;
}
MyStr& operator=(const MyStr& str) {
cout << "operator=" << endl;
if (this != &str) {
this->id = str.id;
this->name = str.name;
}
return *this;
}
~MyStr() {
cout << "析構函數" << endl;
}
};
int main() {
MyStr str1(1,"zxy");
cout << "~~~~~~~~~~~~~~~~~~~~" << endl;
MyStr str2;
str2 = str1;
cout << "~~~~~~~~~~~~~~~~" << endl;
MyStr str3 = str2;
return 0;
}
結果:
MyStr str1(1,"zxy");
MyStr str2;
//賦值 重載=運算符
str2 = str1;
//初始化 調用拷貝構造
MyStr str3 = str2;
參數方面
MyStr& operator=(const MyStr& str) {
cout << "operator=" << endl;
if (this != &str) {
this->id = str.id;
this->name = str.name;
}
return *this;
}
傳入const 修飾的引用
1 const 修飾形參使函數不但能接受非const 實參,也能接受const 實參,傳入的形參由於被const修飾,所以不能被通過形參名修改
2 引用傳遞,少調用一次拷貝構造
3一般我們不希望傳入的實參被修改
返回值
一般是被賦值對象的引用 return *this; (MyStr&)
優點
1 返回時少調用一次拷貝構造,提高效率
2 可以實現連續的賦值
a=b=c
如果返回的類型是值,不是引用,(a=b)=c那麼在執行 a=b時 執行的是=運算符重載,return 後會產生一個匿名對象,這是一個右值,返回這個匿名對象(臨時值),執行a=b後,得到一個臨時右值,再執行=c,就會出錯,右值不能當做左值用。引用既可以當左值,也可以當右值
調用的時機
1 爲一個類對象A賦值,str1=str2;用已經存在的str2初始化str1 (str2=str1)
str2調用=操作符重載
2用其他類型(如內置類型)的值爲其賦值,(隱式類型轉化,前提類A提供其他類型B到類型A的構造函數 A(B))
當爲一個類對象賦值(注意:可以用本類對象爲其賦值(如上面例1),也可以用其它類型(如內置類型)的值爲其賦值,關於這一點,見後面的例2)時,會由該對象調用該類的賦值運算符重載函數。
MyStr str2;
(聲明加初始化,調用無參構造函數)
str1 = str2;(用str2給str1賦值)
MyStr str3 = str2;
用str2初始化str3.調用拷貝構造
提供默認賦值運算符重載函數的時機
當程序沒有顯示提供一個用本類或者本類的引用爲參數的賦值運算符重載函數時,編譯器會自動提供一個賦值運算符重載。
。注意我們的限定條件,不是說只要程序中有了顯式的賦值運算符重載函數,編譯器就一定不再提供默認的版本,而是說只有程序顯式提供了以本類或本類的引用爲參數的賦值運算符重載函數時,編譯器纔不會提供默認的版本。可見,所謂默認,就是“以本類或本類的引用爲參數”的意思。
#include<iostream>
#include<string>
using namespace::std;
class A {
private: int count;
public: A() {
cout << "默認構造" << endl;
}
A(int c) :count(c) {
cout << "有參構造" << endl;
}
//參數是int類型的賦值運算符重載
A& operator=(int count) {
cout << "參數是int類型的賦值運算符重載" << endl;
this->count = count;
return *this;
}
// 拷貝構造
A(const A& a) {
this->count = a.count;
cout << "拷貝構造" << endl;
}
~A() {
cout << this << "析構函數" << endl;
}
};
int main() {
A a1(1);
A a2, a3;
//a2=a1 依然調用編譯器默認提供的賦值運算符重載
a2 = a1;
a3 = 1;
return 0;
}
雖然我們提供了一個參數是int類型的賦值運算符重載,但是a2=a1,編譯器依然提供了默認賦值運算符重載
以類本身或本類引用爲參數的賦值運算符重載,纔是編譯器默認的賦值運算符重載函數
當以上代碼註釋掉 參數類型爲int的賦值運算符重載後,a3=1,調用有參的構造函數
結論:1 當程序沒有顯示提供一個以類本身或本身引用爲參數的賦值運算符重載時,編譯器會默認提供一個
2 當用一個非類A的值對一個類A進行賦值時,如果同時存在匹配的賦值運算符重載和構造函數優先調用賦值運算符重載(如上面的參數是int類型的運算符重載),如果沒有退而求其次,調用匹配的構造函數進行賦值操作。
3 A a3=1; 必須要有int=》A類的類型轉換的構造函數,有參數爲int的賦值運算符重載也沒用
#include<iostream>
#include<string>
using namespace::std;
class A {
private: int count=0;
public: A() {
cout << "默認構造" << endl;
}
A& operator=(const int a) {
this->count = a;
cout << "=運算符重載" << endl;
return *this;
}
~A() {
cout << this << "析構函數" << endl;
}
};
int main() {
//錯誤代碼
A a3 = 1;
return 0;
}
顯示提供賦值運算符重載的時機
1 用非類A的值爲類A對象進行賦值時(當然要是存在相應的構造函數也可不提供)
2用類A的值爲類A的對象進行賦值時,且類A的成員變量含有指針,需要開闢內存,爲了避免淺拷貝,所以必須顯示的重載賦值運算符
深拷貝vs淺拷貝
賦值運算符重載和拷貝構造函數都會涉及到這個問題,只要類A的成員變量裏面有指針,內置類型(如int)不會有問題
淺拷貝就是系統默認提供的拷貝構造函數或者賦值運算符重載只是簡單的把類對象a的數據成員的地址給了b的數據成員,b的數據成員指向了a的數據成員的地址,沒有開闢新的空間(b.name=a.name) MyStr b=a;
帶來的問題
1 修改a.name時,b.name也會隨之變化
2 對 對象a,b析構時,會對同一個內存釋放兩次,導致程序崩潰
解決方法
1 重寫拷貝構造或者賦值運算符重載
2使用std::shared_ptr可以完美解決問題
深拷貝例子
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:
Person() {}
Person(char* name, int age) {
cout << "調用有參構造" << endl;
m_name = (char*)malloc(strlen(name) + 1);
strcpy(m_name, name);
m_age = age;
}
Person(const Person& a) {
cout<<"調用拷貝構造"<<endl;
m_age = a.m_age;
m_name = (char*)malloc(strlen(a.m_name) + 1);
}
~Person() {
cout << "析構函數調用" << endl;
if (m_name != NULL) {
free(m_name);
m_name = NULL;
}
}
char* m_name = NULL;
int m_age = 0;
};
void test01() {
Person p1((char*)"張三", 18);
Person p2(p1);
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
調用有參構造
調用拷貝構造
析構函數調用
析構函數調用
請按任意鍵繼續.
失敗代碼
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括號讓s1和s2變成局部對象,方便測試
Student s1;
Student s2(s1);// 複製對象
}
system("pause");
return 0;
}
Student
~Student 780776
~Student 780776
不然會造成兩個name指針指向同一個內存對象,然後函數調用結束,系統會對同一塊內存進行兩次析構,導致非法內
賦值運算符重載函數只能是類的非靜態成員函數
不能是靜態成員函數,也不可能是友元函數
有人說賦值運算符重載往往需要返回*this,然而友元函數和靜態成員函數沒有this指針,但是可以這麼寫
static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{
……
return str1;
}
不能是靜態成員函數是因爲,靜態成員函數只能操作靜態成員,不能操作非靜態成員,這顯然是不行的
不能是友元函數的原因,假設類沒有顯示提供以類本身或者類本身引用爲參數的賦值運算符操作函數,但是我們假設c++允許我們提供一個友元函數作爲賦值運算符重載函數,但是友元函數不屬於類,所以類內也會默認提供一個賦值運算符重載函數。當執行str2=str1時
編譯器不知道調用哪個,會產生二義性。
所以c++強制規定賦值運算符重載函數必須是非靜態非友元成員函數,所以c++就可以提供默認版本,避免二義性
賦值運算符重載函數不能被繼承
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int X;
A() {}
A& operator =(const int x)
{
X = x;
return *this;
}
};
class B :public A
{
public:
B(void) :A() {}
};
int main()
{
A a;
B b;
a = 45;
//b = 67;
(A)b = 67;
return 0;
}
b=67,不能通過編譯,說明基類的=operator函數沒有被繼承
因爲派生類往往會添加自己的數據成員和成員函數,如果允許派生類繼承基類的賦值運算符重載函數,而且派生類沒有提供,派生類就會調用基類的賦值運算符重載函數,但是基類的這個函數不能處理派生類後添加的數據成員和成員函數,就會有問題。
所以,C++規定,賦值運算符重載函數不能被繼承。 上面代碼中, (A)b = 67; 一句可以編譯通過,原因是我們將B類對象b強制轉換成了A類對象。
賦值運算符重載要避免自賦值
如何判斷賦值者和被賦值者是不同的對象
通過比較他們的地址
if(this!=&str){
..........
}
原因1:
爲了效率,自己給自己賦值沒有意義,所有先判斷if(this!=&str),如false直接return *this
原因2:
如果類的數據成員含有指針,那麼更會帶來問題,假設p=_p,p所指向的通常是new出來的對象,我們賦值前通常要delete掉p的空間,在重新爲p開闢空間,然後進行賦值。如果賦值前沒有釋放掉p的空間,會導致非法內存,如果是自賦值,那麼p和_p是同一指針,在賦值操作前對p的delete操作,將導致p所指的數據同時被銷燬。那麼重新賦值時,拿什麼來賦?
所以,對於賦值運算符重載函數,一定要先檢查是否是自賦值,如果是,直接return *this。
運算符重載的一些例子
左移運算符重載
cout<<xxxxx<<endl;
這種形式來講全局函數更適合左移運算符的重載
#include<iostream>
#include<string>
using namespace::std;
//cout<<A;
class A {
friend ostream& operator<<(ostream& c, A a);
private:int count;
public:
// 默認構造函數
A() {
cout << "默認構造函數" <<this <<endl;
}
//有參構造函數
A(int c):count(c) {
cout << "有參構造函數" << this << endl;
}
//拷貝構造函數
A(const A& a) {
this->count = a.count;
cout << "拷貝構造函數" << this << endl;
}
//賦值運算符重載
A& operator=(const A& a) {
cout<<"賦值運算符重載"<<this << endl;
this->count = a.count;
return *this;
}
~A() {
cout << "A的析構函數" <<this<< endl;
}
};
//全局函數左移操作符重載、
ostream& operator<<(ostream& c, A a) {
c << a.count;
return c;
}
void test() {
A a(1);
cout <<a << endl;
}
int main() {
test();
system("pause");
return 0;
}
前置++,後置++運算符的重載
#include<iostream>
#include<string>
using namespace::std;
//cout<<A;
class A {
friend ostream& operator<<(ostream& c, A a);
friend void test();
private: int count=0;
public:
// 默認構造函數
A() {
cout << "默認構造函數" <<this <<endl;
}
//有參構造函數
A(int c):count(c) {
cout << "有參構造函數" << this << endl;
}
//拷貝構造函數
A(const A& a) {
this->count = a.count;
cout << "拷貝構造函數" << this << endl;
}
//賦值運算符重載
A& operator=(const A& a) {
cout<<"賦值運算符重載"<<this << endl;
this->count = a.count;
return *this;
}
~A() {
cout << " A的析構函數" <<this<< endl;
}
//前置++
A& operator++() {
this->count++;
return *this;
}
//後置++
A operator++(int) {
A tmp = *this;
++*this;
return tmp;
}
};
//全局函數左移操作符重載、
ostream& operator<<(ostream& c,A a) {
c << a.count;
return c;
}
void test() {
A a;
cout<<a<< endl;
cout << "前置++" << " " << ++a << endl;
cout << "後置++" << " " << a++ << endl;
}
int main() {
test();
system("pause");
return 0;
}