1.取地址运算符重载
代码语言:javascript复制 cout << &d1 << endl;
cout << &d3 << endl;
/*
自定义类型想要用运算符都要重载运算符
默认的运算符就是 --
我们自己将对应运算符的重载函数写好了,然后我们用的时候系统自己进行调用
但是我们在这里我们没有对&这个符号进行重载的操作的
但是为什么我们在这里进行&能用呢?
因为这个是默认成员函数,我们不写,编译器默认生成的
就是我们在这里些&d1 &d2
默认调用了两个operator &函数的
为什么说是两个呢?因为两个对象的性质是不一样的
d1是被const修饰的对象,d3是普通对象
普通对象就调用普通的取地址
是const对象的时候调用的函数后面就有个const
所以两个函数是要分开来的
所以取地址成员函数存在两份的,一共普通版本,一个const版本
这个因为是默认生成的函数,所以我们是不需要进行函数的编写的
*/
const成员函数
• 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
• const实际修饰该成员函数隐含的this指针指向的对象,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 constDate* const this
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);//对象实例化
d1.Print();//普通对象进行print的调用,括号内存在隐藏的this指针
const Date d2(2024, 8, 5);
/*
&d ->const Date*
这个this指针的类型是Date*的
我们这里的d2是被const修饰的,d2是不能修改的
但是我们的this指针是被const修饰的
两个是不一样的
所以我们需要将this指针的const放到*左边,但是我们没办法做到这个,所以就在函数的后面加上const来实现这个要求了
这个const在*左边,那么就是修饰对象的,如果在右边的话就是修饰的是指针本身了
那么这里就是修饰的指向的内容,d不能进行修改
我们上面的Print(Date* const this)
那么const修饰的就是指针
这个const是可以进行忽略的
修饰本身的const不涉及权限的放大和缩小
我被我拷贝一份,给你进行赋值操作
又不改变我本身
指向内容的const才会存在权限的放大和缩小的问题的
上面的是const Date *
我指向的内容我都改变不了
const Date *不能给Date *
就属于权限的放大的问题
Date *是可以改变的,但是const Date *是不能改变的
权限被放大了
我们把Print括号内改成const Date*就不存在权限放大的问题了
但是没有办法进行修改
因为this指针是隐藏的,我们是加不了const的
所以祖师爷想了一个办法,选择在函数参数列表后面加上const
这个const修饰的不是this指针本身,而是this指针指向的内容
那么我们在函数参数列表后面加上const的话就相当于
Print(const Date*const this)
const修饰指向内容的时候才涉及到权限放大和缩小的内容
在Print函数后面加上const就是权限的平移
*/
/*
后面加上const普通对象也是可以进行使用的
Date d1(2024, 7, 5);
&d1->Date* 这个Date*可读可写
然后传到Print函数内的this指针变成只能读的
那么就是权限的缩小了
权限不能放大,但是权限能缩小
*/
d2.Print();
return 0;
}
&d2 ->const Date* 这个this指针的类型是Date的 我们这里的d2是被const修饰的,d2是不能修改的,但是指针能修改 但是我们的this指针是被const修饰的,指针不能修改 两个是不一样的 所以我们需要将this指针的const放到左边,但是我们没办法做到这个,所以就在函数的后面加上const来实现这个要求了
加了指针后我们普通对象也能进行函数的调用了
因为我们将普通对象的地址传给this指针,然后this指针只能读不能修改,这个就是权限的缩小了,这个是被允许的
那么我们是否能将所有的成员函数后面都加const呢?
答案是不能
成员函数加上const也表明了你也付出了一定的代价了
被const函数修饰的成员函数我们是不能继续修改内部的成员变量的
不管调用的时普通对象还是ocnst对象
如果我们本身就是要对成员变量进行修改,那么我们就不能加上const了
成员函数里面的变量如果是需要修改的话我们就不能加const
我们现在对我们之前写的日期类进行操作,看看那些成员函数后面可以加const
• 构造函数是不能加const的,加了const我们就不能对成员变量进行修改操作了
• Print函数是可以添加const的
• CheckDate函数也可以添加const,函数内部没有什么需要修改的
•比较大小的运算符重载也是可以进行const的添加的
• =是不能加的,我把我加等到你上面去,修改你的值
• 可以加const
• 和–都不能加,都需要对自己进行修改的操作
•-可以就爱const
•只要不改变自己的都能加const
•流插入和流提取是不能加的,因为都不是成员函数
•成员函数才能加const,因为const修饰的是this指针
•生命和定义都需要进行const的添加
在日期类中,我们现在对d1和d2这两个日期类对象进行相减的操作
我们是一定要在减法函数后面加上const的,对this指针指向的对象进行修饰
因为我们的d1是被const修饰的对象,那么如果传过去的话,this指针是不能接收的
不加const的话,原本我们的d1被const修饰了,不能修改了,然后传到了this指针,能修改了,这个就是权限的放大了,所以我们是一定要在减法函数的后面加上const的
取地址成员函数的重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
我们这里的取地址成员函数是默认生成的,不需要我们进行代码的编写的,默认生成的就够用了
我们就不需要管的
那么什么情况我们还需要写一下取地址运算符的重载呢?
如果我们不想别人取到当前类对象的地址,我们就可以自己进行函数的编写,胡乱返回一个地址
我们写的话编译器就用我们写的函数,不写的话就是默认生成的
返回空指针就取不到我的地址了
对日期类某些函数后面添加const:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
//为什么这里没有缺省值呢?
//因为缺省值在声明和定义都存在的时候,我们只能在声明的时候给
//Date::Date(int year, int month, int day)//类外面定义,我们要制定类域的,不然是无法区分成员函数的
//{
// _year = year;
// _month = month;
// _day = day;
// if (!CheckDate())//如果这个日期不合法的话
// {
// cout << "日期非法,请重新输入->";
// cout << *this;
// }
//}
Date::Date(int year, int month, int day)//类外面定义,我们要制定类域的,不然是无法区分成员函数的
:_year (year)
,_month(month)
,_day(day)
{
if (!CheckDate())//如果这个日期不合法的话
{
cout << "日期非法,请重新输入->";
cout << *this;
}
}
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool Date::operator<(const Date& d) const
{
if (_year < d._year)//年小就小
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
//剩下的条件就是大于了
return false;
}
bool Date::operator>(const Date& d) const
{
//我们已经将小于和等于的操作符重载写出来了,那么我们就直接进行取反,那么剩下的就是大于
return !(*this <= d);
}
//d1<=d2 那么this就是d1的地址,d就是d2
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;//我们这里的基础是我们先将小于和等于的操作符重载的函数写出来
}
bool Date::operator>=(const Date& d)const//小于取反
{
return !(*this < d);
}
bool Date::operator==(const Date& d)const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)const//等于取反
{
return !(*this == d);
}
/*
这些操作符重载是有关系的
我们将小于和等于写出来就能进行剩下的操作符重载的代码书写了
*/
//d1 100天
Date& Date::operator =(int day)//日期加整型是有意义的,帮助我们算多少天以后的日期
{
//d1 =-100 d1=d1-100
if (day < 0)//这里的day<0
{
return *this -= -day;
}
_day = day;
while (_day > GetMonthDay(_year, _month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
{
_day -= GetMonthDay(_year, _month);
//将当前这个月的天数减掉
//减掉之后我们进行月份的增加
_month;
if (_month == 13)//那么就说明当前这一年已经过完了
{
_year ;
_month = 1;//月份变为来年的一月
}
}
return *this;//反正这个结果出了函数是没有销毁的,那么我们直接用引用返回就行了
/*
Date dateObj(2024, 8, 20);
dateObj = dateObj 10; // 将日期增加10天,并且更新 dateObj
我们返回的的就是这个天数增加的对象
*/
}
//这个的前提是有 =的操作符重载的函数
Date Date::operator (int day)const//我们不能改变原先的对象,就是this指针指向的对象
{
Date tmp = *this;//调用拷贝构造,拷贝一个对象出来
tmp = day;//我们上面已经写了 =的操作了,我们直接在这里赋值运用 =的操作
return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
//所以我们用传值返回的操作,不能用引用返回
//Date Date::operator (int day)//我们不能改变原先的对象,就是this指针指向的对象
//{
// Date tmp = *this;//调用拷贝构造
// tmp._day = day;
// while (tmp._day > GetMonthDay(tmp._year, tmp._month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
// {
// tmp._day -= GetMonthDay(tmp._year, tmp._month);
// //将当前这个月的天数减掉
// //减掉之后我们进行月份的增加
// tmp._month;
// if (tmp._month == 13)//那么就说明当前这一年已经过完了
// {
// tmp._year ;
// tmp._month = 1;//月份变为来年的一月
//
// }
// }
// return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
//}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
所以我们用传值返回的操作,不能用引用返回
//
在 存在的情况下实现 =的操作符重载
//Date& Date::operator =(int day)
//{
// *this = *this day;//存在 拷贝
// return *this;
//}
//因为这里是存在拷贝的,而且这下面的先 再 =的话拷贝次数比上面的多,
//所以我们选择先 =再
Date& Date::operator-=(int day)
{
if (day < 0)//这里的day是负数
{
return *this = -day;
}
_day -= day;//原先的天数-我们想要减去的天数
while (_day <= 0)
{
--_month;
if (_month == 0)//如果现在是1月的话,我们--就到0了。实际应该是12月
{
//上一年的12月
_month = 12;
--_year;
}
_day = GetMonthDay(_year, _month);//我们再加上这个月的天数
}
return *this;
}
Date Date::operator-(int day)const
{
Date tmp(*this);//我们进行拷贝构造
tmp -= day;//利用上面我们已经实现的-=
return tmp;
}
// d1 前置 返回 后的值
Date& Date::operator ()
{
*this = 1;
return *this;
}
//d1 后置 返回 前的值
Date Date::operator (int)
{
Date tmp(*this);//先拷贝构造出一个对象
*this = 1;
return tmp;//返回 以前的值
}
//--d1 前置-- 返回--后的值
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//d1-- 后置--
Date Date::operator--(int)
{
Date tmp(*this);//先拷贝构造出一个对象
*this -= 1;
return tmp;//返回 以前的值
}
//两个日期相减就是得到的之间相差的天数
int Date::operator-(const Date& d)const
{
/*
思路一:我们算出两个日期距离当年的的1月1号有多少天
如果是同一年的话那么直接就是大的减小的
但是如果不是一年的话,我们直接就算出距离多少年*/
/*
思路二:让小的日期进行 ,直到走到了大的日期
加了多少次就是多少天,不需要考虑其他的条件*/
Date max = *this;//假设d1大
Date min = d;//d假设d2小
//上面的是拷贝构造
int flag = 1;
if (*this < d)//运用这个小于操作符重载就能看出谁小
{
//这里的是赋值,都是存在的对象
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (min != max)
{
min;
count;
}
return count * flag;//这个存在第一个小第二个大,第二个小,第一个大的两种情况
//返回值可能是负数
}
//流插入---输出
//void Date::operator<<(ostream& out)
//{
// out << _year << "年" << _month << "月" << _day << "日" << endl;
//}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//流提取
istream& operator>>(istream& in, Date& d)
//这里是不能加const的,因为我们提取的值要写到对象d里面去
{
while (1)//直到我们输入了一个正确的日期我们就能退出
{
cout << "请一次输入年月日:";
in >> d._year >> d._month >> d._day;
if (d.CheckDate())
{
break;
}
else
{
cout << "日期非法,请重新输入" << endl;
}
}
return in;
}
2. 再探构造函数
• 之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
下面的就是另一种构造函数的写法
冒号开头,逗号分割
代码语言:javascript复制Date::Date(int year, int month, int day)//类外面定义,我们要制定类域的,不然是无法区分成员函数的
:_year (year)
,_month(month)
,_day(day)
{
if (!CheckDate())//如果这个日期不合法的话
{
cout << "日期非法,请重新输入->";
cout << *this;
}
}
这个初始化列表和函数体内初始化是没什么区别的
两种方法都是可以的,这个对于日期类是没有区别的
括号里面放我们要初始化的值的大小,或者给上一个表达式也是可以的
但是存在一些特殊情况只能用初始化列表进行初始化操作
存在三类:
1.没有默认构造的类类型成员变量
就是像我们两个栈实现队列一样,我们必须对于队列内的两个栈的话我们需要有对应的构造函数
我们是没有机会在函数体内进行成员变量的初始化操作了,所以我们只能进行初始化列表的初始化操作,因为两个栈是在队列的里面的,所以我们是没有机会进行函数体内的初始化操作
2.const成员变量,只能在初始化列表里面进行初始化操作
3.引用成员变量
• 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。
成员变量只能进行初始化一次
• 引⽤成员变量,const成员变量,没有默认构造的类类型成员变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
• C 11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
如果显示给了初始化的话,那么就按照显示的,如果我们在声明变量的时候进行了变量的缺省操作的话,并且我们给了值的话,那么就按照我们给的值进行初始化操作
代码语言:javascript复制class Time
{
public :
Time(int hour=0)
: _hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public :
Date(int year = 2, int month = 2, int day = 2)
: _year(year)
, _month(month)
, _day(day)
{
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
Time _t=10; // 我们初始化列表中没有对_t进行初始化的操作,但是我们也能调用默认构造的
};
int main()
{
int i = 0;
Date d1(2024,5,1);
d1.Print();
return 0;
}
//通过调试我们能知道我们尽管在初始化列表中没有进行_t的初始化的操作,但是我们仍然进行了_t的初始化操作
//调用了上面的默认构造
/*
C 11⽀持在成员变量声明的位置给缺省值,这个缺省值
主要是给没有显⽰在初始化列表初始化的成员使⽤的
我们可以在声明的时候给上缺省值,这个声明是给初始化列表的
如果我们在初始化列表中给出了初始化的话,那么我们下面变量的缺省操作就会被省略了,直接用上我们给的值
显示给了初始化,那么就不会尊崇声明的时候的缺省值
缺省值就是最卑微的,如果什么都没给的话,那么就是用缺省值
*/
• 尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C 并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
• 初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致
构造函数太复杂了,我们在这里进行深化一下
为什么引⽤成员变量、const成员变量、没有默认构造的类类型成员变量这三种必须进行列表初始化操作呢?
因为引用成员变量的话我们在定义的时候就必须进行初始化操作的
const变量是不能修改的,只有一次修改的机会,就是在定义的时候
没有默认构造的话,我们要调用的话就必须进行传参的操作
初始化有需要进行传参,传参就必须在初始化列表进行了
代码语言:javascript复制class Time
{
public :
Time(int hour)
: _hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public :
Date(int& x, int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _t(12)
, _ref(x)
, _n(1)
{
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; // 没有默认构造
int& _ref; // 引⽤
const int _n; // const
};
int main()
{
int i = 0;
Date d1(i);
d1.Print();
return 0;
}
尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表
对于这句话的来说的话
对于上面说到的特殊的三类,我们能否使用缺省值进行初始化操作呢
没显示写的话我们有缺省值也是可以的
在声明变量的时候直接声明缺省值就行了,我们会在初始化的时候进行缺省值的调用操作的
以后得构造函数初始化成员尽量使用初始化列表
优先就是显示初始化
没有缺省值就是调用对应类型的默认构造函数
• 初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致
这个顺序主要是采用我们在列表中对变量的声明的顺序的,不是按照这个初始化列表的顺序进行初始化的
这个题的话我们是不能选择这个存在2的,因为我们显示初始化了,那么就和缺省值没关系了
我们也不能选择一,因为我们对于这个变量初始化的话我们不是按照初始化列表的顺序进行初始化操作的,而是按照变量声明的时候进行初始化的
那么这个题的话我们是先进行a2的初始化的,我们的_a1和a2都是随机值,我们传的a是1,但是我们先对a2进行初始化, _ a2(_ a1)这个就是随机值初始化随机值,然后结果就是随机值
然后我们对a1进行初始化操作,就是用1对a1进行初始化的操作
那么这个题就选择D
所以我们就建议声明的顺序和初始化列表的顺序是一致的
3. 类型转换
• C ⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
• 构造函数前⾯加explicit就不再⽀持隐式类型转换。
• 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持
如果我们不想这个隐式类型转换发生的话,我们就在构造函数前面加上这个explicit
当我们现在存在一个栈的话,我们对这个栈进行数据插入的操作,我们上面的就是创建对象,利用对象进行数据的传输,这个栈是存储A类的数据的,所以我们每次传输数据之前我们需要创建一个A对象并进行赋值,然后利用栈对象将这个A对象的数据传过去
但是现在的话我们是不需要进行A类对象的创建操作的,我们直接传就行了
但是现在C 支持这个内置类型隐式类型转换为类类型对象
那么我们直接写
我们直接利用隐式类型转换的操作
从内置类型转换为自定义类型的数据的话我们需要有这个对应的构造函数的
自定义类型之间能否进行转换的操作呢?
A类型的数据转换为B类型的数据,同样也是需要构造函数存在的
这里的aa3会产生一个临时对象然后进行转换的操作的
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class A
{
public :
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
: _a1(a1)
{}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public :
B(const A& a)
: _b(a.Get())
{}
private:
int _b = 0;
};
int main()
{
// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
// 编译器遇到连续构造 拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();
A aa3 = { 1,1 };//调用这个多参数的构造我们需要用花括号括起来才能进行初始化构造
B b = aa3;//用A类型的对B类型的数据进行赋值操作的话,我们需要存在这个构造函数
return 0;
}
关于C 的类型转换,我将从基础到进阶为你整理一个清晰的文章,涵盖常见的类型转换方式、用法以及注意事项,帮助你梳理知识点。
1. 隐式类型转换
隐式类型转换是由编译器自动完成的,通常发生在不同类型的变量之间进行操作时。C 会在合理的情况下自动转换类型,如将 int
转换为 double
。
示例:
代码语言:javascript复制int a = 10;
double b = 5.5;
double c = a b; // a 被自动转换为 double 类型
编译器会自动将 int
类型的 a
转换为 double
,以便与 b
进行运算。
注意事项:
- 隐式转换虽然方便,但也可能会带来精度丢失或效率问题。例如
double
转换为int
会丢失小数部分。
2. 显式类型转换
显式类型转换通常通过强制转换(Type Casting)实现,程序员通过明确的语法告诉编译器进行类型转换。
C 提供了四种强制类型转换方法:
-
static_cast
-
dynamic_cast
-
const_cast
-
reinterpret_cast
2.1 static_cast
这是最常用的类型转换,适用于大多数基本类型之间的转换。它在编译期进行检查,不涉及运行时开销。
示例:
代码语言:javascript复制int a = 10;
double b = static_cast<double>(a); // 将 int 类型转换为 double 类型
2.2 dynamic_cast
dynamic_cast
主要用于多态类型(带有虚函数的类)之间的转换,用于将基类指针或引用转换为派生类指针或引用。
示例:
代码语言:javascript复制class Base {
virtual void func() {}
};
class Derived : public Base {
void func() override {}
};
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 基类转换为派生类
dynamic_cast
在运行时检查转换是否成功,若失败,指针会返回 nullptr
。
2.3 const_cast
const_cast
用于去掉或添加 const
属性。它主要用来修改常量变量,但请注意,不要试图通过它修改真正的常量。
示例:
代码语言:javascript复制const int a = 10;
int* p = const_cast<int*>(&a); // 去掉 const 属性
*p = 20; // 这样做有风险,修改真正的常量可能导致未定义行为
2.4 reinterpret_cast
这种转换最不安全,它将一种类型的指针或引用转换为另一种类型的指针或引用,主要用于底层操作,例如将 int
类型的指针转换为 char*
。
示例:
代码语言:javascript复制int a = 42;
char* p = reinterpret_cast<char*>(&a); // 将 int* 转换为 char*
3. C风格类型转换
C 风格的强制转换是通过 (new_type)
的语法进行的。
示例:
代码语言:javascript复制int a = 10;
double b = (double)a; // C 风格类型转换
然而,C 风格的类型转换不如 C 提供的四种类型转换安全,因为它没有进行任何类型检查,容易导致错误和不明确的转换。
4. 类型转换操作符
在类中,可以自定义类型转换操作符,实现类与其他类型之间的转换。
示例:
代码语言:javascript复制class Complex {
public:
operator double() const { return real; }
private:
double real;
};
Complex c;
double d = c; // 自动调用 Complex 类的 double 类型转换操作符
5. 注意事项
- 尽量避免使用隐式类型转换,特别是当涉及不同范围或精度的类型时。
- 使用显式转换时,优先选择
static_cast
,因为它既安全又高效。 -
dynamic_cast
只适用于带有虚函数的类层次结构,使用时需要确保基类和派生类的转换关系。 - 避免过多使用
reinterpret_cast
,它容易引发难以追踪的错误。
6. 总结
C 提供了多种类型转换方式,从隐式到显式,灵活地应对不同的需求。理解并合理使用这些转换方式,不仅能够提升代码的安全性和可读性,还能避免不必要的错误。
希望这篇文章能帮助你理解C 中的类型转换,如果有具体的问题或不清楚的地方,欢迎继续讨论!
4.static成员
•⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
•⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
• 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
• 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
• 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表
静态成员对象是存在静态区的,公共的区域,共享的,不需要在构造函数中进行初始化
我们这个静态成员变量是要放在类外面进行初始化的
我们在外面声明类域进行初始化操作
如果我们不通过成员函数的话我们是访问不到的,因为这个静态成员变量是一个私有的
我们是不可以直接通过类域进行这个静态成员变量的打印操作的
所以我们是需要通过成员函数将这个静态成员变量进行返回的操作的
如果这个静态成员变量是公有的话那么我们在类外面通过类域就能进行调用了
我们有静态成员变量也有静态成员函数的,静态成员函数是没有this指针的,所以我们不用指定对象进行调用了
静态成员函数只能访问其他的静态成员,不能访问非静态的,因为静态成员函数没有this指针
我们可以通过制定类域的方式进行访问,也可以通过对象点的操作进行访问
静态成员变量我们是不能在声明的时候给缺省值的,因为这个缺省值是给初始化列表的
静态的成员变量是不会走初始化列表的
https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?
代码语言:javascript复制class Sum
{
public:
Sum()//构造函数,每当创建了Sum类的对象就会自动进行调用例如
{
_ret =_i;
_i;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i=1;
int Sum::_ret=0;
class Solution {
public:
int Sum_Solution(int n)
{
Sum arr[n];//创建一个有n个元素的数组
return Sum::GetRet();
/*
当这个数组被创建时,数组的每一个元素都会触发 Sum 类的构造函数,从而实现累加运算。*/
}
};
对于这个题的话我们利用这个静态成员变量以及静态成员函数以及构造函数的地洞调用的操作
因为c是全局的,所以我们在main函数之前就可以进行初始化操作了
对于析构函数的话,后定义的先进行析构操作,这里我们先析构ab,因为cd的声明周期是全局的,一定在main韩函数结束以后才会进行析构的操作
局部的静态是会先进行析构的,全局后析构
在C 中,静态成员变量(static
member variables)是属于类本身的,而不是某个具体的对象。这意味着静态成员变量在所有对象之间共享,且只有一个实例。为了正确使用静态成员变量,通常需要在类的定义中声明它们,并在类的外部进行初始化。
以下是静态成员变量声明和初始化的步骤:
1. 静态成员变量的声明
在类内部声明静态成员变量时,使用 static
关键字。
示例:
代码语言:javascript复制class MyClass {
public:
static int staticVar; // 静态成员变量声明
};
在类 MyClass
中,我们声明了一个静态的整型变量 staticVar
。注意,这里仅仅是声明。
2. 静态成员变量的初始化
静态成员变量需要在类外进行初始化,初始化时不使用 static
关键字,只需要指定其类型和类作用域。
示例:
代码语言:javascript复制#include <iostream>
class MyClass {
public:
static int staticVar; // 静态成员变量声明
};
// 在类外部进行初始化
int MyClass::staticVar = 10;
int main() {
// 访问静态成员变量
std::cout << "Static Variable: " << MyClass::staticVar << std::endl;
// 修改静态成员变量
MyClass::staticVar = 20;
std::cout << "Updated Static Variable: " << MyClass::staticVar << std::endl;
return 0;
}
3. 解释:
- 在
class MyClass
中,我们声明了静态成员变量staticVar
。 - 然后,在类的外部(在
main
函数之外的全局范围内),我们通过MyClass::staticVar
进行静态成员变量的初始化。在这个过程中,我们指定它的初始值为10
。 - 之后,静态成员变量可以通过类名
MyClass::staticVar
进行访问和修改。
4. 静态成员变量的特点:
- 静态成员变量只在类的作用域内声明一次,但会在类的所有对象中共享。
- 即使没有创建对象,也可以通过
类名::静态成员变量
进行访问。 - 它只需要在类外初始化一次,无论创建多少个对象,静态成员变量都不会被重新初始化。
- 静态成员变量的生命周期与程序的生命周期相同,直到程序结束时才会被销毁。
5. 静态成员变量的访问方式:
- 可以通过类名来访问静态成员变量:
MyClass::staticVar
。 - 也可以通过类的对象来访问静态成员变量,但这并不是推荐的方式。
示例:
代码语言:javascript复制MyClass obj;
std::cout << "Static Variable via object: " << obj.staticVar << std::endl; // 不推荐
6. 静态常量成员变量
对于常量静态成员变量(如 const static
),可以在类内部进行初始化,但前提是它的类型必须是整型或枚举类型。
示例:
代码语言:javascript复制class MyClass {
public:
static const int constVar = 100; // 可以在类内部直接初始化
};
如果静态常量成员是非整数类型,则必须像普通静态成员一样,在类外进行初始化。
示例:
代码语言:javascript复制class MyClass {
public:
static const double constDouble;
};
// 类外初始化
const double MyClass::constDouble = 3.14;
总结:
- 静态成员变量在类内声明,在类外进行初始化。
- 静态成员变量属于类本身,而不是某个具体的对象。
- 静态成员变量可以通过类名访问,也可以通过对象访问(但不推荐)。
- 静态常量成员变量如果是整型或枚举类型,可以在类内直接初始化;否则,必须在类外进行初始化。
如果有更多关于静态成员变量的具体问题,欢迎继续讨论!
5. 内部类
•如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类。
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。
一般的话我们是将类定义在全局的,但是我们现在提出内部类这个概念,就是将一个类定义在另一个类的里面
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class A
{
private:
static int _k;//静态成员变量没有存储在对象里面,存储在静态区里面的
//所以我们在计算大小的时候我们是不会计算这个静态成员变量的
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
private:
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;//A的大小是4字节,只计算了A这个类中的_h变量
//并且没有计算B这个类的大小
return 0;
}
内部类的话,B是A的内部类,但是两个类是独立的,并非B是A的成员
只是在访问的时候收到A的类域的影响
外部类定义的对象中不包含内部类的成员
我们在主函数不能直接访问B这个类,B这个类收到A这个类的类域的限制了
所以我们需要指定下类域
除了外部类域的限制,内部类还受到了访问限定符的限制
如果这个内部类对于外部类是私有的话,那么我们这里是会报错的
因为外部类默认是内部类的友元,所以我们内部类是可以访问外部类的私有
这个_k是静态的,我们可以直接进行访问操作
如果是非静态的话,我们通过对象可以直接进行访问的
在C 中,内部类(nested class) 是指在一个类的定义中嵌套另一个类。这意味着一个类可以包含另一个类的定义,称为内部类。内部类有自己独立的作用域和访问权限,它的设计目的通常是为了逻辑上的封装和代码的组织性。
内部类的特点与使用总结
- 作用域管理:
内部类的作用域被限定在外部类的作用域中。这意味着在外部类之外,必须通过外部类的名称来引用内部类。
定义内部类的语法通常为:
代码语言:javascript复制
class Outer { public: class Inner { // 内部类的定义 }; };
代码语言:javascript复制 在外部使用内部类时需要指定完整的作用域:
```C
Outer::Inner obj;
- 访问外部类的成员:
内部类默认不能直接访问外部类的私有成员,即使它被定义在外部类中。要访问外部类的私有成员,通常需要通过外部类的公开接口或通过友元机制。
例如:
代码语言:javascript复制
class Outer { private: int x; public: Outer() : x(10) {}
代码语言:javascript复制class Inner {
public:
void display(Outer& o) {
std::cout << "Outer x: " << o.x << std::endl; // 可以通过引用访问外部类的成员
}
};
};
代码语言:javascript复制3. **访问控制和封装**:
- 内部类与外部类是相互独立的类,它们的访问控制规则是独立的。内部类有自己的访问权限(public, private, protected),而这些访问权限与外部类没有直接关系。
- 外部类和内部类的成员访问权限是独立的。例如,外部类的私有成员无法直接被内部类访问,反之亦然,除非有特别的访问权限(如友元关系)或通过外部类/内部类的公共接口。
4. **组织代码的结构**:
- 内部类经常用于在逻辑上属于某个外部类的部分行为进行封装。例如,当某个类的内部结构复杂,需要多个类配合时,将某些辅助类放在主类内部可以提高代码的可读性。
- 内部类有助于避免全局命名空间污染,使类的结构更加清晰。例如,一个外部类可能包含多个只在该类内有意义的类型,把它们作为内部类来定义,便于维护和组织代码。
5. **外部类与内部类的独立性**:
- 内部类的对象是可以独立于外部类的对象创建的。这意味着你可以在不创建外部类对象的情况下创建内部类对象:
```C
Outer::Inner innerObj; // 创建内部类对象,而不需要外部类对象
- 但在某些设计中,内部类可能需要一个外部类对象来操作外部类的成员,通常通过构造函数或其他机制传递外部类的引用或指针。
- 友元关系:
外部类可以将内部类声明为友元,反之亦然。这样,内部类或外部类能够访问对方的私有或保护成员。例如:
代码语言:javascript复制
class Outer { private: int y; public: class Inner { friend class Outer; // 声明外部类为友元 }; };
代码语言:javascript复制### 内部类的使用场景
7. **封装辅助类**:当一个类需要多个小的辅助类来帮助实现某些功能时,这些辅助类可以被放在外部类的内部以表示逻辑上的从属关系。例如,迭代器类通常会作为容器类的内部类实现。
8. **限制类的作用域**:通过将类定义为内部类,开发者可以限制其作用域,使它只能在外部类相关的上下文中使用,减少全局命名空间污染。
9. **复杂系统的分层设计**:在设计复杂系统时,内部类有助于将类之间的关系分层,从而提高代码的可维护性和可读性。
### 例子
```C
#include <iostream>
class Outer {
private:
int a;
public:
Outer() : a(5) {}
class Inner {
public:
void showOuter(Outer& outer) {
std::cout << "Accessing Outer class private member: " << outer.a << std::endl;
}
};
};
int main() {
Outer outerObj;
Outer::Inner innerObj;
innerObj.showOuter(outerObj); // 输出:Accessing Outer class private member: 5
return 0;
}
总结
- 内部类 是一个用于在逻辑上将相关类封装在一起的强大工具,有助于代码的组织性和可读性。
- 内部类有独立的访问控制规则,默认不能直接访问外部类的私有成员,但可以通过接口或友元机制访问。
- 内部类使代码的分层设计更加清晰,减少命名空间污染,适用于辅助类或有明确从属关系的类设计场景。
6. 友元
•友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
• 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
• ⼀个函数可以是多个类的友元函数。
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
• 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
• 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤
友元函数是什么以及友元函数的介绍
友元函数(friend function)是C 中的一种特殊函数,它允许一个类之外的函数访问该类的私有(private)和保护(protected)成员。通常,类的私有成员是不能直接被外部函数访问的,必须通过公共成员函数来访问。然而,友元函数是一种例外,它可以直接访问类的私有和保护成员。
友元函数的特点:
- 可以访问类的私有和保护成员:友元函数虽然定义在类的外部,但可以访问类的私有和保护成员。
- 不是类的成员函数:友元函数并不属于类的成员函数,因此在调用时不需要通过类的对象来调用,可以直接使用。
- 声明使用
friend
关键字:要将一个函数声明为友元函数,必须在类中用friend
关键字来显式声明。
友元函数的语法:
代码语言:javascript复制class MyClass {
private:
int privateData;
public:
MyClass(int value) : privateData(value) {}
// 声明一个友元函数
friend void showPrivateData(MyClass obj);
};
// 定义友元函数
void showPrivateData(MyClass obj) {
// 友元函数可以直接访问 MyClass 的私有成员
std::cout << "Private Data: " << obj.privateData << std::endl;
}
int main() {
MyClass obj(42);
showPrivateData(obj); // 访问类的私有数据
return 0;
}
友元函数的使用场景:
- 运算符重载:有时候需要通过非成员函数实现运算符重载,而这些函数需要访问类的私有数据,此时友元函数非常有用。例如,重载
<<
或>>
操作符时,通常将其声明为类的友元函数。 - 类之间的紧密合作:如果两个类需要相互访问私有成员,而又不希望破坏封装性,可以将一个类的函数声明为另一个类的友元。
注意事项:
- 友元函数破坏封装性:友元函数虽然能访问私有成员,但它破坏了类的封装性。因此,使用友元函数要慎重,一般只有在确有必要时才使用。
- 友元关系不是对称的:如果类A是类B的友元,类B并不会自动成为类A的友元。
- 友元关系不是传递的:如果类A是类B的友元,类B是类C的友元,类A并不会自动成为类C的友元。
使用友元函数的关键在于在需要时合理地允许外部函数访问类的内部数据,同时保持类设计的整体封装性。
这里的意思就是说B就是A的友元
A是B的友元,B是C的友元,那么A是C的友元吗?
这个是不对的,友元的关系是不能传递的
并且A是B的友元
但是B不是A的友元,因为友元的关系是单向的,不具有交换性
在C 中,友元(friend) 是一个特殊的功能,允许一个类将其他类或函数声明为它的友元,以便它们可以访问该类的私有成员和保护成员。
通常,类的私有成员(private)和保护成员(protected)是不能被外部访问的,只能在类的内部使用。然而,有时候为了方便合作类之间的交互,或为了方便某些全局函数能够直接访问类的内部成员,可以使用“友元”来打破这个访问限制。
友元的特点:
- 友元不是类的成员:尽管友元可以访问类的私有成员,但它并不是类的成员函数。它是外部的,只是类授予了它访问权限。
- 友元关系是单向的:如果类A将类B声明为友元,类B可以访问类A的私有成员,但反过来类A并不能访问类B的私有成员,除非类B也将类A声明为友元。
- 友元关系不能继承:如果一个基类将某个类或函数声明为友元,这个友元权限不会自动传递给派生类。
友元的使用场景:
- 友元函数:一个非成员函数可以被声明为类的友元,这样它可以直接访问该类的私有和保护成员。
class MyClass {
private:
int data;
public:
MyClass() : data(0) {}
friend void showData(MyClass&); // 声明 showData 为友元函数
};
void showData(MyClass& obj) {
// 访问 MyClass 的私有成员
std::cout << "Data: " << obj.data << std::endl;
}
- 友元类:一个类可以将另一个类声明为友元类,允许该友元类的成员函数访问它的私有成员。
class ClassA {
private:
int secret;
public:
ClassA() : secret(42) {}
friend class ClassB; // 将 ClassB 声明为友元类
};
class ClassB {
public:
void revealSecret(ClassA& obj) {
// 访问 ClassA 的私有成员
std::cout << "Secret: " << obj.secret << std::endl;
}
};
- 友元成员函数:一个类的某个成员函数可以被声明为另一个类的友元。
class AnotherClass {
private:
int value;
public:
AnotherClass() : value(100) {}
friend void MyClass::accessValue(AnotherClass&); // 声明 MyClass 的成员函数为友元
};
总结:
友元机制主要用于打破C 类的封装性限制,使得非成员函数或其他类可以访问类的私有和保护成员,但这种权限应该谨慎使用,因为它破坏了面向对象编程的封装性,可能导致代码难以维护。
7.匿名对象
• ⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
• 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
}
对于有名对象的话,他们的析构函数是在main函数结束以后进行的
但是这个匿名对象,生命周期在当前这一行,这一行运行结束之后就进行析构函数,就是下一行进行析构
而且匿名对象参数无参的时候我们也是需要将这个括号带着的
代码语言:javascript复制class A
{
public :
A(int a = 0)
: _a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
void func(A aa=A(1))
{}
int main()
{
func();
//有名对象
A aa1(1);
A aa2;
//匿名对象
A(1);
A();
//这种事有名对象调用函数的操作
Solution s1;
cout << s1.Sum_Solution(10) << endl;
//下面是匿名对象调用函数
cout << Solution().Sum_Solution(10) << endl;
const A& r = A();
return 0;
}
//对于有名对象的话,他们的析构函数是在main函数结束以后进行的
//但是这个匿名对象,生命周期在当前这一行,这一行运行结束之后就进行析构函数,就是下一行进行析构
//而且匿名对象参数无参的时候我们也是需要将这个括号带着的
有名对象和匿名对象调用类中的函数的区别
在给函数进行缺省值的设置的时候我们可以用到这个匿名对象
同时,匿名对象也是可以进行引用操作的,但是匿名对象是具有常性的,我们是需要在左边添加这个const的,因为这个匿名对象有常性,我们不能随便进行修改的操作
并且匿名对象被引用之后生命周期是会被延长的,所以这个匿名对象如果被引用的话是伴随着引用的销毁而销毁的
如果这个匿名对象销毁之后,但是引用还在,这个引用就变成了野引用了
这里的话这个匿名对象的生命周期就跟着这个r走了
匿名对象(Anonymous Object)是指在编程中创建一个不具有显式名称的对象。这种对象通常被立即使用,并且不需要在变量中保存引用。匿名对象通常用于简化代码结构,特别是在需要临时对象但不需要它们长期存在或复用的情况下。
匿名对象的特点
- 没有名称:匿名对象没有明确的变量名,它们直接在创建的地方使用。
- 短生命周期:通常,它们的生命周期很短,通常仅限于创建它们的上下文。
- 简化代码:匿名对象有助于避免为临时对象命名,减少不必要的代码。
常见使用场景
匿名对象在某些编程语言中很常见,尤其是在面向对象编程中。以下是几个常见的场景:
- 方法参数:在传递对象作为方法参数时,直接创建匿名对象而不需要事先定义它。
public class Demo {
public void printData(Person person) {
System.out.println(person.getName());
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.printData(new Person("John", 30)); // 直接创建匿名对象
}
}
- 短暂的计算:在需要临时对象来执行某些逻辑时,使用匿名对象。
int result = new Calculator().add(10, 20); // 创建匿名Calculator对象执行加法
- 初始化列表:在某些语言中,匿名对象还用于在构造函数中初始化对象或集合,例如在JavaScript中。
let student = {
name: "Alice",
age: 20,
grade: "A"
};
匿名对象的优点
- 简洁性:避免为短暂的对象创建冗余变量,简化代码。
- 更好的性能:在某些情况下,匿名对象可能避免了不必要的对象引用,帮助垃圾回收更快地回收内存。
注意事项
匿名对象虽然能简化代码,但也可能让代码变得难以调试或维护,因为没有明确的对象引用。如果多个地方需要使用相同的对象,建议使用具名对象。
8.对象拷⻉时的编译器优化
•现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。
• 如何优化C 标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化
代码语言:javascript复制#include<iostream>
using namespace std;
class A
{
public :
A(int a = 0)
: _a1(a)
{
cout << "A(int a)" << endl;
}
A
(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return* this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{}
int main()
{
//优化
A aa1=1;
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 隐式类型,连续构造 拷⻉构造->优化为直接构造
f1(1);
// ⼀个表达式中,连续构造 拷⻉构造->优化为⼀个构造
f1(A(2));
cout << endl;
return 0;
}
在对象拷贝时,编译器通常会进行优化,以减少不必要的拷贝和提高程序的执行效率。以下是几种常见的编译器优化策略:
1. 返回值优化
当函数返回一个对象时,编译器可以直接在调用方的内存空间中构造返回的对象,而不是先在函数内部构造,然后再拷贝到调用方。这种优化可以避免一次不必要的对象拷贝。
- 示例:
MyClass createObject() {
return MyClass();
}
在没有RVO时,MyClass()
会先在函数内部创建,然后拷贝到调用方。开启RVO后,MyClass
可以直接在调用方的内存空间中构造。
2.移动语义
在C 11及之后的版本中,移动语义是一种用于减少不必要拷贝的优化。当对象被移动时,编译器会通过“偷取”资源的方式来避免深拷贝。
- 移动构造函数和移动赋值运算符允许编译器从源对象中“移动”资源,而不是复制它们。这在临时对象或对象需要从另一个作用域转移时特别有用。
- 示例:
MyClass obj1;
MyClass obj2 = std::move(obj1); // 通过移动语义“偷取”资源,而不是拷贝
3. 拷贝省略
C 17引入了强制的拷贝省略规则,意味着在某些情况下,即使没有定义移动构造函数或移动赋值运算符,编译器也会跳过拷贝操作,直接构造目标对象。
- 示例:
MyClass createObject() {
MyClass obj;
return obj; // 编译器可以省略拷贝或移动
}
4. 内联优化(Inline Expansion)
对于小的、频繁调用的函数,编译器可能会选择内联展开函数代码。这样不仅减少了函数调用的开销,同时还可能使编译器在内联的上下文中进行更多的对象优化。
- 示例:
inline MyClass createObject() {
return MyClass();
}
5. 对象合并与内存重用
对象合并是一种编译器优化,它尝试将多个对象的生命周期进行分析,如果它们不会同时存在于内存中,编译器可以将它们分配在同一块内存空间中,从而减少内存占用。
6. 懒惰拷贝
也称为写时拷贝(Copy on Write, COW)。当多个对象引用同一资源时,编译器会延迟执行真正的拷贝,直到有一个对象尝试修改该资源时才进行拷贝。这避免了不必要的深拷贝。
7. 循环展开与向量化优化
在对象拷贝的循环中,编译器可能会进行循环展开和向量化优化,将循环中的多个对象拷贝操作合并或并行化,以提高性能。
这些优化大部分依赖于编译器本身的优化级别设置(如-O2
, -O3
),程序员也可以通过编写合理的代码和使用现代C 特性(如移动语义)来帮助编译器进行更好的优化。