目录
this指针
this指针的引出
当我们定义一个类的时候,比如一个日期类
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针解决该问题,即:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的
this
指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
所以就解决了成员函数访问的问题,每个对象都有他自己的this指针,所以就可以精确的访问到对象的成员。
当调用成员函数的时候,将对象的地址当作实参传给成员函数。
this指针特性
- this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
void Print(Date* const this)
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递- this指针不能显示写,就是我们不能在成员函数的形式参数上把this指针写出来,但是在成员函数内部可以使用。一般也不写出来用,因为默认编译器就会加上。
this指针存在栈上面,因为他是一个形参,一个局部变量,但是有些编译器,比如vs可能会用寄存器传递。
this指针理解
下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
//cout << _a << endl;
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
运行结果是正常运行,因为this指针存储的是类的地址,此时A类的指针p存的是nullptr,所以此时的类的地址就认为是nullptr。那么this的值就是nullptr,即0。但是我们并没有非法访问成员变量和成员函数,成员函数并不在p指向的地址处,成员函数在公共代码区,如果我们在成员函数中,对成员变量进行的访问,那么此时就会报错了,因为nullptr处没有对象,不是我们开辟的空间。此时this指向nullptr处即认为此处有对象,但是对象的成员函数在公共代码区,且没有非法操作,所以会正常运行,但是如果运行注释的那一行便会报错。
构造函数
当我们创建一个类的时候,class Date {};
如果什么都没有,称为空类,但是空类并不是什么都没有。
任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
其中构造函数就是类中的一个特殊函数。
概念引入
对于下面Date类
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
这就可以使用构造函数
构造函数是一个特殊的成员函数,
名字与类名相同
,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
class Date
{
public:
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "_" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2014, 10, 1);
d2.Print();
return 0;
}
- 从上述代码运行结果可以看出,构造函数构成函数重载,因为有两个同名的函数,一个带参数一个不带。
- 无参的构造函数,调用时不能带括号,因为这样无法和无参的函数声明区分,函数声明也是类型加函数名加括号。
- 上述的代码也可以写成带缺省参数的函数。但是不能一个给全缺省,一个无参,这样的话,编译器不知道调用的哪个。
- 构造函数在创建对象的时候就要执行,而且必须执行,如果有构造函数却不执行的话,就会报错。比如下面
class Date
{
public:
// 1.无参构造函数
/* Date()
{
_year = 1;
_month = 1;
_day = 1;
}*/
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "_" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
/*d1.Print();
Date d2(2014, 10, 1);
d2.Print();*/
return 0;
}
我们将无参的构造函数屏蔽掉,如果不执行带参的构造函数,就会报下面的错误。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。即没有构造函数,编译器会自己生成一个构造函数。
- 生成的默认的构造函数对内置类型不做处理,对自定义类型,去调用相应的默认构造函数。自定义类型即struct / class。内置类型即 int / char等。
- C++11,委员会对针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值(缺省值)。
下面代码对上述两个特性做解释
class A
{
public:
A()
{
cout << "A" << endl;
_a = 0;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << "Date" << endl;
}
private:
int _year;
int _month;
int _day;
A a;
};
int main()
{
Date d1;
return 0;
}
代码中Date没有写构造函数,那么就去执行默认构造函数,默认构造函数只对自定义类型进行处理,去执行他的默认构造函数,他的默认构造函数就是输出 A 。
值得注意的是,如果有自定义类型,即使对象里面有构造函数,自定义的构造函数也会被执行。
总结:
分析一个类型成员和初始化需求,需要我们写构造函数我们就自己写,不需要 ,就用编译器生成的。绝大多数场景下我们要自己写构造函数。
并不是编译器生成的构造函数才叫默认构造函数,无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
即:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。推荐写全缺省的构造函数。
无论是不是默认构造函数,如果对象里面含自定义类型,总是会执行自定义类的默认构造函数。此时自定义类型里面必须有默认构造函数,否则编译器无法执行。
析构函数
概念
析构函数和构造函数的功能大致相反,构造函数用来初始化对象,析构函数用来清理对象,不是销毁,是清理,局部对象销毁工作是由编译器完成的。而清理相当于一个收尾工作。
特性
析构函数也是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。即,自己没有定义析构函数,会生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 多个对象的析构根据构造的先后顺序的相反顺序执行,但是static会改变声明周期,会在局部的最后一个执行析构。
class A
{
public:
~A()
{
cout << "~A" << endl;
}
};
class Date
{
public:
void Print()
{
cout << ".." << endl;
}
private:
int _a;
A a;
};
int main()
{
Date date;
return 0;
}
对上述代码,Date没有析构函数,在main结束的时候调用默认的析构函数,默认的析构函数和默认的构造函数类似,只处理自定义类型的析构函数。所以最后会调用~A
。
对于有显示析构函数的对象,不仅会调用自己的析构函数,对于自定义类型的也会调用,因为要保证对象可以别清理(收尾)。
对于定义两个对象,后定义的先执行析构,先定义的后执行析构。如果是全局的,则全局的最后执行析构,所有的全局也满足先定义后析构。
析构函数的作用在上面代码中并不能很好的体现,在栈里面体现的则更为明显。写了析构函数就可以不用手动区销毁开辟的空间,在析构函数里面销毁就好,因为生命周期结束,会自动执行析构函数。即:有资源申请时,一定要写析构函数,否则会造成资源泄漏,比如Stack类。
拷贝构造
认识拷贝构造
拷贝构造函数是一种特殊的构造函数
,它在创建对象时,是使用同一类,之前创建的对象来初始化新创建的对象。
需要区分于默认构造函数,拷贝构造函数不是默认执行的,我们将一个对象当作参数时,编译器才会执行拷贝构造函数。
通过下面代码理解
class A
{
public:
A()
/*要添加默认构造函数,
因为有了拷贝构造函数,在创建a1对象的时候,编译器不会再生成默认构造函数,
若不执行拷贝构造,就必须执行默认构造函数,否则报错*/
{
cout << "A" << endl;
}
A(A& a)
{
cout << "copy" << endl;
}
};
int main()
{
A a1;
A a2(a1);
return 0;
}
上述代码中,a1作为实参传递给类里面的拷贝构造函数的形参a。用来初始化对象a2,只是我们没有加成员变量,体现的不那么明显。
拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象。
- 复制对象把它作为参数传递给函数。
- 复制对象,并从函数返回这个对象。
拷贝构造为什么要用引用传参
对于上述拷贝构造的使用可以简单的理解为,如果一个对象被作为参数传递的,就会执行拷贝构造。
看下面几个例子:
class A
{
public:
A()
{
cout << "A" << endl;
}
A(A& a)
{
cout << "copy" << endl;
}
};
int main()
{
A a1;
A a2 = a1;
A a3;
a3 = a2;
return 0;
}
我们知道,对于创建一个新的对象,如果不指定执行哪个构造函数,就执行默认构造函数,可以看到输出了两次A,说明a1和a3执行了默认构造函数,而a2执行了拷贝构造。但是当我们执行a3 = a2
的时候并不执行拷贝构造。
于是得出结论,当一个对象在创建的时候用另一个对象对其初始化时,就会执行默认构造,如果是已经创建过的对象,再用其他对象对其赋值,就不会执行拷贝构造。 注意初始化和赋值的区别,刚创建的时候是初始化,创建后再改变是赋值。
再看下面代码:
class A
{
public:
A()
{
cout << "A" << endl;
}
A(A& a)
{
cout << "copy" << endl;
}
};
void Func(A a)
{}
int main()
{
A a1;
Func(a1);
return 0;
}
当对象作为参数时,去调用了拷贝构造,该怎么解释这一现象呢?
当实参传递给形参的时候,创建一个临时变量作为形参,实参作为参数传给形参。这里传递的情况可以看作,
A a = a1;
于是通过上面的结论便可以知道,这个语句是要调用拷贝构造的。
通过上面的例子,我们再去解释,为什么要用引用传参。
假设不使用引用是成立的,分析下面代码:
class A
{
public:
A()
{
cout << "A" << endl;
}
A(A a)
{
cout << "copy" << endl;
}
};
int main()
{
A a1;
A a2(a1);
return 0;
}
当我们将a1的值传给a2对象的时候,a1作为实参传递给拷贝构造函数中的形参a。拷贝构造函数要创建一个临时变量,假设为tmp1,那么就执行tmp1 = a1;要执行这个语句就要将a1作为实参传到tmp1中的拷贝构造函数中,tmp1的拷贝构造函数要创建一个临时变量tmp2接收a1,那么就执行tmp2 = a1;要执行这个语句就要将a1作为实参传到tmp2中的拷贝构造函数中…,我们发现陷入了死循环,这就造成了无限递归现象。于是不能使用这样的方式进行传递,如果使用指针的话就太麻烦,不合适。而且不符合拷贝构造函数的规定写法,那样写就是构造函数了。
使用引用的话,本质上还是指针,但是使用起来就会很方便了。而且不会造成无限递归的现象,因为传递的是一个地址,函数体内部直接使用 " . "引出来即可。
class A
{
public:
A()
{
cout << "A" << endl;
}
A(A& a)
{
cout << "copy" << endl;
_a = a._a;
_b = a._b;
}
private:
int _a;
int _b;
};
使用引用也不是万无一失的,如果我们的动作是初始化,那么就不能修改被拷贝的值,但是引用如果操作失误就容易把被拷贝的值修改掉。所有要拷贝构造函数的参数前面加
const
。在上面的演示代码里面Func()函数,参数用引用的话就需要视情况而定,如果不改变原来的值,就需要加const,如果不用引用的话,就会取调用拷贝构造,浪费时间。
默认拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。 若要使用,同样需要我们调用。默认的拷贝构造函数对象对于内置类型按内存存储按字节序完成拷贝,就是按照二进制位,一位一位地完全拷贝过去。这种拷贝叫做浅拷贝,或者值拷贝。
默认拷贝构造函数的特点:
- 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,就是按照二进制位,一位一位地完全拷贝过去。
- 如果有自定义类型,那么当我们调用拷贝构造的时候,除了第一条外,还会调用自定义类型的拷贝构造。但是如果我们写了拷贝构造,就不会调用自定义类型的拷贝构造,此时自定义类型的成员变量是默认值。如果要对自定义类型的成员变量修改就要把自定义类型的成员变量设为公有。
class A1
{
public:
A1() = default;
A1(const A1& A)
{
cout << "A1(const A1& A)" << endl;
j = A.j;
}
private:
int j;
};
class A2
{
public:
A2() = default;//强制增加默认构造函数。
/*A2(const A2& A)
{
cout << "A2(const A2& A)" << endl;
}*/
private:
int i;
A1 a1;
};
int main()
{
A2 a2;
A2 a2_2(a2);
return 0;
}
上述代码没有我们写的拷贝构造,就执行默认的拷贝构造,其中内置类型a2_2的 i 就拷贝a2的 i ,自定义类型就去执行自定义类型的拷贝构造函数。上述代码就是a2_2 的 a1 的 j 拷贝 a2 的 a1 的 j 。对象a2_2的值整体拷贝a2的值,自定义类型也是,从a2 的 a1 的 j 传参到a2_2 的 a1 的拷贝构造函数。
但是如果我们自己写了拷贝构造函数,就不会默认执行自定义类的拷贝构造。
第一幅是默认拷贝构造,第二幅把注释删去了,自己写的拷贝构造。
深拷贝
默认构造函数是浅拷贝。如果我们只是普通的拷贝,像日期类,只有年月日,这样的拷贝,仿佛就不需要我们写拷贝构造函数了。直接使用默认的拷贝构造函数就好了。但是看下面代码:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
//CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
Stack s2(s1);
return 0;
}
上述代码没有写拷贝构造函数,使用默认的拷贝构造。跑起来就会崩溃掉,因为s1的_array拷贝给了s2,s2在执行析构函数的时候,执行了一次free,把s1 malloc的空间释放掉了,后来s1又释放了一次,所以就崩溃了。
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
如何写深拷贝:
对深拷贝的处理其实就是对传入的地址处理,需要我们自己开辟空间,使用此对象开辟的地址。
Stack(const Stack& st)
{
DataType* tmp = (DataType*)malloc(st._capacity * sizeof(DataType));
if (tmp == nullptr)
{
perror("mallco fail");
exit(-1);
}
memcpy(tmp, st._array, sizeof(DataType) * st._size);
_array = tmp;
_capacity = st._capacity;
_size = st._size;
}
上述代码就是栈的深拷贝,将之前的栈的内容拷贝到新的栈,使用memcpy
函数,拷贝到我么自己的空间。
总结:
拷贝构造特性:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。- 编译器生成的默认拷贝构造函数可以完成字节序的值拷贝了,一旦涉及到资源申请
时,则拷贝构造函数要自己写,否则就是浅拷贝。- 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。
通过下面代码理解运算符重载
class Date
{
public:
Date(int y, int m ,int d)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return true;
}
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 12, 28);
Date d2(2024, 6, 28);
bool ret1 = d1 < d2;
bool ret2 = d1.operator<(d2);
int i = 10;
int j = 9;
bool ret3 = i < j;
cout << ret1 << ret2 << ret3 << endl;
return 0;
}
上述代码就是运算符重载的演示,bool ret1 = d1 < d2;
中 <
被转化函数去执行。因为我们无法直接和类进行比大小,所以定义了一个函数bool operator<(const Date& d)
来进行比较,但是使用的时候,直接使用 <
就可以,其实就是程序员写了一个成员函数,让程序员用起来像是直接比较,其实是编译器自己转换了。除此之外还可以写成其他符号,加号,减号等等。方便我们使用的一个符号。上述代码bool ret1 = d1 < d2;
和bool ret2 = d1.operator<(d2);
是一样的。执行的时候有一个转化的过程。
总结就是定义一个函数,并告诉C++编译器,当遇到该运算符时就调用此函数来行使运算符功能。这个函数叫做运算符重载函数(常为类的成员函数)。
和内置类型比较不同,内置类型比较是CPU直接就能完成比较,但是自定义类型,编译器并不知道其比较方式,所以要我们自己来控制他的比较方式。
在汇编里面也可以看出区别,把自定义类型要去调用operator函数,而内置类型直接就进行比较,是一种基于指令集的比较方式。
赋值运算符重载
class Date
{
public:
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 12, 28);
Date d2(2024, 6, 28);
d1 = d2;
d1.Print();
return 0;
}
上述代码就是将=
进行重载,让我们使用的时候看起来就相当于赋值。
我们想让自定义类型赋值的行为和内置类型一样,然后看一下内置类型的行为。
上述代码可以看出,只打印了100,这个100其实就是 i 的返回值,在赋值的时候,100的值赋给 j ,然后 j 返回自己的值,然后 i 接收 j 的返回值,最后返回 i 的值,最后输出的就是 i 的值。
我们想让自定义类型也是这样的行为,所以我们赋值运算符重载函数要有返回值,所以可以这样改:
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
传this指针的解引用,就是这里的左操作数了,就是d1 = d2
中的d1。所以这样传出的就是d1的值了。但是这里返回值是Date,相当于传值返回。传值返回返回的是d1的拷贝,而不是d1本身。这样就会调用他的拷贝构造,很浪费时间。
所以最好是下面这样的改变,用引用返回
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
此时就可以执行d1 = d2 = d3
这种连续赋值了。
此时返回的就是d1本身了,这样就比较危险,可能会无意间改变d1的值
其实并不会,当执行
d1 = d2 = d3
时,d3作为d2的参数传入Date& operator=(const Date& d)
,然后d2返回他的别名,作为了d1的参数,中间并不存在修改的d2的事情,当执行完以后,返回的是d1的别名,此时就没有其他的接收的变量了。
我们再看下面代码
从上述代码可以看出,i = j 语句,返回的就是 i 本身,不然无法对 i 进行赋值。
同样的,我们上面赋值运算符重载也是这样的逻辑,返回的d1的别名,d1就能被修改
默认赋值运算符重载
C++中,对类对象进行操作时,我们就不能只是简简单单地,对类对象用 = 进行操作。
当我们没有自己设计等号运算符的重载函数,编译器会自动生成一个浅拷贝的赋值运算符的重载函数。与拷贝构造类似。
只是简单地将一个对象的内存数据赋值给另一个对象,如果这个对象成员变量引用了外部资源时,那么这两个对象的成员变量都指向这个空间,当这两个对象生存周期结束时,进行析构,那么就会崩溃,对同一块内存我们free了两次。
注意:
1.内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
2.赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。即,不能将赋值运算符重载定义为全局的,但是其他运算符可以。因为其他运算符没有默认的重载。
其实 默认赋值运算符重载在上面写《拷贝构造为什么要引用传参》的时候就已经使用过。
前置运算符重载++ 和 后置运算符重载++
当构成前置++和后置++时,如果只是一个Date& operator++()
无法形成两个运算符,所以要进行特殊处理。特殊处理的方式有很多种,可以使用Date& ++operator()
,但是在设计的时候并没有这样设计。而是Date& operator++(int)
构成重载,来区分前置和后置的++,Date& operator++()
为前置++,Date& operator++(int)
为后置++。 --操作符也是一样的。
Date& operator++()
{
*this += 1;//运行+=运算符重载
return *this;
}
Date operator++(int)
{
Date tmp = *this;//运行=运算符重载
*this += 1;//运行+=运算符重载
return tmp;
}
可以看出,因为后置++要返回+1之前的值,所以要用tmp承接原来的对象,但是tmp是临时变量,所以不能返回引用,只能返回他的拷贝,此时要调用拷贝构造,就会比前置++执行的慢。
流提取运算符<<重载
之前使用cout都是只能输出内置类型的变量,但是自定义类型的变量无法输出。于是可以运用运算符重载,自己写自定义类型怎么输出。
class Date{
.
.
.
};
Date date;
int b;
cout << b << endl;//正确
cout << date << endl;//错误
需要知道的是cout
是全局的ostream
类型的一个对象,后面参数要用ostream类型的引用。
class Date
{
public:
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;//内置类型直接支持流插入
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 12, 28);
d1.operator<<(cout)
d1 << cout;
//cout << d1;//报错
return 0;
}
可以像上述代码一样写<<
运算符的重载,参数out作为cout的引用,当然传入的要是cout才行。内置类型直接支持流插入,函数内通过cout的引用输出。但是使用的时候,发现和之前的cout的用法是反着的,因为双操作数的类型要匹配,左操作数是第一个参数,右操作数是第二个参数,所以d1要卸载<<
前面,cout作为参数。实际是d1.operator<<(cout)
,但是这样是不符合我们使用习惯的,控制台流入对象d1里面。
本质原因是:作为成员函数重载,this指针占据了第一个参数。Date就必须是左操作数。
那么如果要写成我们习惯的用法cout << d1,就不能这样写,就只能写成全局的函数。这样改变参数的顺序。
class Date
{
public:
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int _year;
int _month;
int _day;
private:
};
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
int main()
{
Date d1(2024, 12, 28);
cout << d1;
return 0;
}
像上述代码,在类外面定义,就没有隐含的this指针,这样就可以第一个参数写成ostream类型。但是这样就不能访问私有成员,只能将私有成员写为公有才行。还有另外的方法就是写成友元函数,但是还没学,不展开说明。
上述的写法还是有一些问题,因为之前cout支持cout << i << j << k << ednl;
但是上述代码并不支持,cout不像赋值时从右往左,赋值运算符,i = j = k;是从右往左赋值,k 赋值给 j ,返回值是 j 。然后 j 赋值给 i ,返回值是 i 。但是cout流插入是从左往右,cout << i << j << k << ednl;
i 先插入给cout,这里是一个函数调用,然后有一个返回值。返回值作为左操作数,j 再进行流插入。
那么怎么改为cout << d1 << d2 << d3 << ednl;
呢?
其实很简单,直接使上面写的函数有一个ostream类型的返回值就好了,这样返回值作为左操作数,就相当于再次调用了这个函数。
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out
}
int main()
{
Date d1(2024, 12, 28);
Date d2(2024, 12, 29);
cout << d1 << d2;
return 0;
}
需要知道的是,内置类型之所以可以直接cout是库里面直接写好的
通过上面写的,cout就支持了任意类型的输出。这是与printf的不同之处。
流插入运算符<<重载
同样的,我们也可以对流插入运算符重载,cin是istream类的一个对象。
istream& operator>>(istream& in, Date& d)
{
cout << "输入日期:" << endl;
in >> d._year >> d._month >> d._day;
return in;
}
与流提取不同的是,流插入时的参数不需要加const,因为我们要修改d的成员变量的值,同样也是引用传参,原因也是我要修改d的成员变量的值,这里可以联想到原来使用的scanf
的参数是传的指针,原因同样是要修改他的值。同样的为了可以多次插入流,要返回 in 的引用。
const
在C语言中:
const修饰变量n后,保护了变量n,使其不能被赋值修改。
但可以使用指针修改,但是此时也是会报警告的。
但是在C++中const的语法更加严格,如果还像上述一样写的话会直接报错。
再看下面代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();//报错,this指针的权限被放大
}
此时d2.Print();
这句代码是无法运行的。
这是因为C++对const的限制很严格,一旦const修饰了变量或者对象不能被修改,也不能通过指针修改。而Print();
里面有隐含的this指针,this指针并没有呗const修饰,可能会造成对象被修改。所以无法运行。
为了使我们写的代码可以运行,就要进行修改,用const修饰this。但是我们知道this指针本身是隐含的指针,不能在形参和实参中体现,只能在函数内部使用。
由于这种问题,于是在设计的时候便是一种特殊的方式void Print() const
在函数后面加const,这样写的隐含的意思就是void Print(const Date* this)
,即不能通过this指针修改对象。
我们将const修饰的 “成员函数” 称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
于是代码就是下面这样:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();//正确
}
一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员了(包括 const 成员变量和 const 成员函数),因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。
总结:
- const对象不可以调用非const成员函数,权限放大了。
- 非const对象可以调用const成员函数,权限缩小了。
- const成员函数内不可以调用其它的非const成员函数,权限放大了。
- 非const成员函数内可以调用其它的const成员函数,权限缩小了。
总的来说就是权限放大和缩小的问题。
值得注意的是:
我们可以通过强转去强制的修改const修饰的变量或者对象,因为一旦加强转,就代表我们允许这样带来的风险,C++允许这样做。
const int i = 100;
int* pi = (int*) &i;
const Date d1(2024, 11, 28);
Date* pd1 = (Date*)&d1;
取地址及const取地址操作符重载
这两个重载函数是最开始所说的默认成员函数的最后的两个重载函数。
大致代码就是下面这样:
class A {
public:
A* operator&()
{
return this;
}
const A* operator&() const
{
return this;
}
};
说明:
1.一般情况下这两个取地址重载函数是不写的,因为默认生成的和我们写的是一样的,除非有一些奇葩需求,比如返回一个假地址。
A* operator&()
{
int i = 100;
return (A*)i;
}
上述代码我们取一个A类的对象的地址就是一个假地址,是 i 的地址。
2.不写的话,会默认有两个,也就是上面那两个,之所以是两个是因为根据类型使用不同的取地址符。没有用const修饰的对象在取地址的时候使用的就是A* operator&()
,有const修饰的对象使用的就是const A* operator&() const
。
3.const A* operator&() const
中的const,后面的const修饰的是this,表示不能通过this修改对象,而且要取const修改的对象的指针,成员函数也必须用const修饰,这一点在上面已经说过了。前面的const是因为this指针是const修饰的,所以返回值也必须是const修饰的指针类型,即const A*
。
4.我们要取const修饰的对象的地址必须用const修饰的指针变量承接,除非对对象加强转,这个在上面也有提到。
5.看下面两次运行
右边注释了非const的取地址重载,下面取地址时报错,这意味着没有生成默认的非const的取地址重载。左边写了非const的取地址重载,但是对d1取地址时没有报错。但是我们知道const修饰的对象只能访问const修饰的成员。这两次就很奇怪,右边报错左边却不报错。于是得出这样的猜测:
写了const修饰的取地址重载,不生成非const修饰的取地址重载。但是写了非constt修饰的取地址重载, 默认生成constt修饰的取地址重载。
6.定义为全局的重载就不会在生成局部的重载,但是有一些需要注意的地方
- 如果全局的的重载定义的是const限制的,那么对象是否是const限制的都会使用全局的那个const限制的重载函数。如下:两个输出都是0xFFAD0000
class Date
{
public:
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
const int* operator&(Date j)//定义全局的重载,就不再生成局部的重载
{
return (int*)0xFFAD0000;
}
//int* operator&(Date& i)//定义全局的重载,就不再生成局部的重载
//{
// return (int*)0xFFFFFFF;
//}
int main()
{
const Date d1(1029, 2, 2);
Date d2(1029, 2, 2);
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
- 如果全局的的重载定义的是非const限制的,那么对象若是const限制的会使用默认生成的const限制的重载函数。但是如果对象非const限制,那么会使用全局的。
下面代码输出的结果
class Date
{
public:
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//const int* operator&(Date j)//定义全局的重载,就不再生成局部的重载
//{
// return (int*)0xFFAD0000;
//}
int* operator&(Date& i)//定义全局的重载,就不再生成局部的重载
{
return (int*)0xFFFFFFF;
}
int main()
{
const Date d1(1029, 2, 2);
Date d2(1029, 2, 2);
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
以上两种情况都是在VS2022下试验得出的结论。对比大概就是允许权限缩小,但是不允许权限放大,且上面两个函数不允许同时写为全局的重载,因为重载是根据参数进行的,两个参数一样,所以不能重载。