个人主页:企鹅不叫的博客
专栏
- C语言初阶和进阶
- C项目
- Leetcode刷题
- 初阶数据结构与算法
- C++初阶和进阶
- 《深入理解计算机操作系统》
- 《高质量C/C++编程》
- Linux
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
【初阶与进阶C++详解】第一篇:C++入门知识必备
【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件
【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)
作用是初始化对象
函数名与类名相同
无返回值,不用写void
构造函数是私有的,对象实例化时编译器自动调用对应的构造函数。
构造函数可以函数重载
构造函数定义
class Date { public : // 1.无参构造函数 Date () {} // 2.带参构造函数 Date (int year, int month , int day ) { _year = year ; _month = month ; _day = day ; } private : int _year ; int _month ; int _day ; };
构造函数调用(参数在对象后面,实例化后自动调用)
//带参数 Date d2(2022,15,15); d2.Print(); //带部分参数 Date d2(2022); d2.Print(); //不带参数 Date d2; d2.Print();
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 。内置类型/基本类型:int/char/double/指针,自定义类型:class/struct去定义类型对象。默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理(默认生成的构造函数去调用自定义类型的构造函数来初始化)。
**总结:**如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数,如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数 ,总之,不传参就可以调用的都叫默认构造函数,上面三种默认构造函数特点是不用传参就可以调用的。**我们一般用全缺省。**我们不写编译器会自动生成构造函数,对于内置类型成员不做处理,对于自定义类型成员回去调用它的默认构造函数。
下面这种情况就不会报错,d2满足无参,可以运行。
class Date { public : // 1.无参构造函数 Date () {} // 2.带参构造函数 Date (int year, int month , int day ) { _year = year ; _month = month ; _day = day ; } private : int _year ; int _month ; int _day ; }; //直接调用 Date d2;
针对编译器自己生成默认成员函数不初始化的问题。
给的是缺省值,编译器自己生成默认构造函数用,不是初始化,这里是声明,声明没有空间。private: int _size = 0;
总结:一般情况一个C++类,都要自己写构造函数,一般只有少数情况可以让编译器默认生成。
1.类里面成员都是自定义型成员,并且这些成员都提供了默认构造函数。
2.如果还有内置类型成员,在声明时给了缺省参数。
对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作,类默认生成析构函数,内置类型不做处理,自定义类型成员回去调用它的析构函数。
class Queue{ public: Queue() { cout<<"Queue"<<endl; _a=(int*)malloc(sizeof(int)*4); _size=0; _capacity=4; } void Print() { cout<<this<<": "; cout<<"size: "<<_size<<" "; cout<<"_capacity: "<<_capacity<<endl; } ~Queue() { //析构函数 free(_a); _a=nullptr; _size=_capacity=0; cout<<"distory:"<<this<<endl; } private: int* _a; int _size; int _capacity; };
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值,不能函数重载。
3.一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(内置类型不会处理,自定义类型会去调用它的析构函数)
4.对象生命周期结束时(就是他所在的域),C++编译系统系统自动调用析构函数。
栈里面定义对象,析构顺序和构造顺序是相反的,后定义的先析构
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
使用:只有栈这样的类需要写拷贝构造,其他不用
Date d1; Date d2(d1);
定义:
Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
1.拷贝构造函数时构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须使用引用传参(用引用来化简,用引用不改变变量的话,记得加const),使用传值会引发无穷调用(构造一个对象需要同类型对象初始化,同类型的对象初始化需要调用拷贝构造)(内置类型是直接拷贝,自定义类型对象,拷贝初始化规定要调用拷贝构造完成)
下面对于第二个Func()函数不是将d1的值拷贝给d,而是调用拷贝构造,第一个由于要传参,所以先调用拷贝构造然后再调用Func()
//调用拷贝构造 void Func(Data d){ } //不调用拷贝构造,别名 void Func(Data& d){ } int main (){ Data d1(2022.5.15); Func(d1); }
3.若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫浅拷贝或者值拷贝,栈这种类不能调用这种浅拷贝,两个栈会指向同一个空间,这块空间析构时会释放两次,程序会崩溃。
4.那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了(浅拷贝)
5.类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。
练习题:
以下代码共调用多少次拷贝构造函数: ( )
class weight
{
public:
Widget f(Widget u)
{
Widget v(u);
Widget w=v;
return w;
}
}
main(){
Widget x;
Widget y=f(f(x));//1
Widget x;
weight ret = f(x);//2
}
- 整个程序分别在,x初始化u、u初始化v、v初始化w、w返回时,注意w返回时初始u不在调用拷贝构造函数,第二次调用 f()函数时,拷贝构造和构造函数直接变成拷贝构造,其他不变,所以总体次数为4+3=7次.
- 同理这里就是四次,return的时候,构造一个临时对象,临时对象再去拷贝构造
//定义一个对象再去传参 weight x; f(x);//或者f(weight()) //定义一个对象再去调用函数 solution st; st.func(10);//或者世界solutin().func(10);
Date(2022, 5, 28)//匿名对象 Date d1(2022,5,26); Date d2(2022,5,27); Date d3(d1); //拷贝构造,一个存在的对象去初始化另外一个要创建的对象 d2 = d1; //赋值重载/复制拷贝,两个已经存在对象之间赋值,调用的还是拷贝构造 A a1= 2; //隐式类型转换,原本是A(2) ->A a1(A(2)) 现在是优化直接构造,构造一个临时对象,临时对象再去拷贝构造
总结:一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,一般编译器都会优化,合二为一
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构
6、因此析构顺序为B A D C
内置类型可以直接用运算符,自定义类型不能直接用各种运算符,所以要运算符重载
函数原型:返回值类型 operator操作符(运算符操作数)
定义:(由于访问的参数都是私有的,一般我们将此函数放到类里面,并且成员函数有一个隐含的this指针,所以将此函数放到类里面就要少些一个参数,但是其实没必要拷贝呀,所以用引用,记得引用加上const)
bool operator==(const Date& d) // 编译器实际处理 bool operator==(Date* this, const Date& d) { return _year == d._year; && _month == d._month && _day == d._day; }
调用:(如果有两个函数一个是类外面的运算符重载,一个是类里面的运算符重载,则优先调用类里面的运算符重载)
if (d1.operator == (d2)){ cout << "==" << endl; } //编译器会处理成对应重载运算符调用 if(d1.operator(d2)) , 编译器实际处理if(d1.operator(&d1, d2)) if(d1 == d2){ cout << "==" << endl; }
- 不能通过连接其他符号来创建新的操作符,没有这个运算符就不能用:比如operator@
- 不能对内置类型使用,只能用自定义类型
- 用于内置类型,不能修改原来的操作符原有的含义,不能把+改成-
- 作为形参时比操作数目少1的成员函数,因为默认操作符有this
- :: 、 sizeof 、 ?: 、 . 、 .* 这五个运算符不能重载
Date d1(2022,5,20); Date d2(2022,5,21);//拷贝构造:一个存在的对象去初始化另外一个要创建的对象 Date d3(d1); //赋值重载/复制拷贝:两个已经存在的对象进行赋值,调用的还是拷贝构造 d3 = d2 = d1; Date d4 = d1;//这是拷贝,因为d4还在初始化 Date d4; d4 = d1;//这是赋值,因为d4已经完成初始化了
为了能连续赋值,将上面d2的值赋值给d3,下面函数得到的是d2的地址,所以返回*this(d2指向的对象),d2类型是Date,所以返回的是Date,由于返回会产生拷贝并且出了作用域对象还在所以我们用 Date& 作为返回减少拷贝构造,返回不能加const,因为会导致不能再给此返回值赋值(不能再作为左操作数了),为了防止自己给自己赋值,我们用this(d2)判断&d(d1)是否相等(地址比较),对于传入参数必须加const,防止权限放大
但是如果我们不写赋值,他会按照拷贝构造赋值,拷贝构造是默认的,所以对于日期类,我们不用拷贝构造和赋值运算符重载
d2 = d1; // d2.operator = (&d2, d1);//左边式子相当于右边这个 Date& operator=(const Date& d){ if(this != &d){ _year = d._year; _month = d._month; _day = d._day; } return *this; }
赋值运算符特点
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
前面是计算当月的天数,注意的是定义和声明分开要注意函数作用域,频繁调用数组可以放到static里面,构造函数在定义的时候不要缺省值,声明的时候可以给,拷贝构造,函数记得有一个隐藏参数this,第二个参数必须用const Date&类型,记得引用不然会一直循环
//判断闰年 bool Date::IsLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } //获取当月天数 int Date::GetMonthDay(int year, int month) { assert(year >= 0 && month < 13 && month > 0); //加static,此函数频繁调用,首次开辟在静态区,之后不会再开辟数组了 static int MonthDayArrray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month == 2 && IsLeapYear(year)) { return 29; } else { return MonthDayArrray[month]; } } //构造函数 Date::Date(int year, int month, int day) { if (year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else { cout << "日期非法!" << endl; } } //拷贝构造函数 Date::Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
下面是一些条件运算符,
//赋值运算符重载 Date& Date::operator=(const Date& d) { if (this != &d) { //防止给自己赋值 _year = d._year; _month = d._month; _day = d._day; } return *this; } bool Date::operator<(const Date& d) { if ((_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day)) { return true; } else { return false; } } //以下都是复用操作 bool Date::operator>(const Date& d) { return !(*this <= d); } bool Date::operator>=(const Date& d) { return !(*this < d); } bool Date::operator!=(const Date& d) { return !(*this == d); } //*this就是d1, d就是d2 //d1 <= d2 bool Date::operator<=(const Date& d) { return *this < d || *this == d; }
特别说明以下复用,不用~,因为 ~ 是按位取反, !是是逻辑取反
//以下都是复用操作 bool Date::operator>(const Date& d) { return !(*this <= d); }
并且不可以写成以下函数,因为此函数被调用时,是d.operator>(*this);,第二个函数调用时,原本d是只读的,调用第一个函数时,变成可读可写的了,造成权限的放大,解决方法是给两个函数后面都加上const。
原则:成员函数内只要不改变成员变量,建议都加constbool Date::operator>(const Date& d) { return true; } bool Date::operator<(const Date& d) { return d > *this; }
特别说明,关于this指针访问
//函数还是成员函数,只是在类中声明,在类外面定义,所以还是可以访问私有 //下面this_year访问的不是类里面的_year,类里面的_year只是声明,没有空间,我们访问的一定是某个对象的成员 //所以我们通过d1地址访问的d1,通过d2别名访问d2 //d1 == d2 bool Date::operator==(const Date& d) { //有个隐藏参数this return _year == d._year //相当于this->_year == d._year && _month == d._month//相当于this->_month == d._year && _day == d._day;//相当于this->_day == d._year }
判断下面两种方法,一种是+=复用+,另外一种是+复用+=,哪一种方式更优?
首先第一种+=复用+,我们知道在+函数里面有两次拷贝构造(返回ret和拷贝*this),每次调用+=都会调用到+
第二种+复用+=,+函数固定有两次拷贝构造,但是+=函数没有调用拷贝构造
综上,第二种方式更优
重点:下面函数this是形参,出了作用域会销毁,但是我们没有返回this,我们返回的是*this
//出了作用域*this还在,可以用引用返回,虽然this不是全局变量,但是至少出了这个函数之后this还在 Date& Date::operator+=(int day) { *this = *this + day; return *this; } //这里不能用引用,用引用就是返回ret别名,但是出了函数就被销毁了 Date Date::operator+(int day) { //调用拷贝构造防止,我们直接修改this指向对象的值 Date ret(*this); ret._day += day; while (ret._day > GetMonthDay(ret._year, ret._month)) { ++ret._month; ret._day -= GetMonthDay(ret._year, ret._month); if (ret._month == 13) { ++ret._year; ret._month = 1; } } return ret; } //出了作用域*this还在,可以用引用返回,虽然this不是全局变量,但是至少出了这个函数之后this还在 Date& Date::operator+=(int day) { _day += day; while (_day > GetMonthDay(_year, _month)) { ++_month; _day -= GetMonthDay(_year, _month); if (_month == 13) { ++_year; _month = 1; } } return *this; } //这里不能用引用,用引用就是返回ret别名,但是出了函数就被销毁了 Date Date::operator+(int day) { Date ret(*this); ret += day; reutrn ret; }
//d1-d2 int Date::operator-(const Date& d) { int flag = 1; Date min = *this; Date max = d; if (min > max) { min = d; max = *this; flag = -1; } int n = 0; while (min != max) { ++min; ++n; } return n * flag; }
//加参数是后置,不加参数是前置 Date& operator++() { *this += 1; return *this; } //后置与前置的区别是返回值不同 Date operator++(int) { //不能给缺省参数,参数是为了区分前置和后置,一般写法都是省略形参 Date ret(*this); *this += 1; return ret; } Date operator--(int) { Date ret(*this); *this -= 1; return ret; } Date& operator--() { *this -= 1; return *this; }
下面代码,d1.Print();可以整成运行,但是Func中的函数不能正常运行,Print函数原型是void Pinrt(Date* const this),this指针不能被修改,但是this指针指向的对象可以被修改,
void Func(const Date& d) { d.Print(); //d1.Print(&d);类型是const Date*,this指向的内容不能被修改,然后传给Print权限放大 } void Test() { Date d1(2002.2.3); d1.Print();//d1.Print(&d1); 传送过去的参数类型是Date* Func(d1); } Print(){ cout << year <<endl;//其实year原型是 this->year }
在函数名后面加 const 用法
void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } //原来 void Print()函数是这样 void Print(Date* const this),加上const后相当于变成现在这样了 void Print(const Date* const this)
- const对象可以调用非const成员函数吗? 不可以,权限放大
- 非const对象可以调用const成员函数吗?可以,权限缩小
- const成员函数内可以调用其它的非const成员函数吗?不可以,权限放大
- 非const成员函数内可以调用其它的const成员函数吗? 可以,权限缩小
总结:成员函数中不修改成员变量的成员函数加上const
下面两个是默认成员函数,编译器自动生成
Date* operator&() { return this ; return nullpter;//这样别人就看不到你的地址了 } const Date* operator&()const { return this ; return nullpter;//这样别人就看不到你的地址了 }
<<:流插入, >>: 流提取,内置类型库里面都函数重载好了
友元函数在类里面声明,在public上面,加上friend就好了,
class Date { //友元函数在类外面可以访问私有成员,为了可以连续插入,返回值必须和输入的值一样,两个参数第一个是cout,第二个是待插入元素 friend std::ostream& operator << (std::ostream& out, const Date& d); friend std::istream& operator >> (std::istream& in, Date& d); public: };
不能将定义部分放到头文件里面,会在main源文件和调用的源文件展开,会生成两个.o文件,main源文件和调用的源文件里面都有符号表的定义,链接的时候就会冲突。所以全局函数不能在头文件中包含,不然可能会在多个源文件中定义,所以要定义在源文
定义部分:
//流插入,将自定义函数放到全局变量中,直接在类里面定义规范使用方法,然后在类里面声明友元 std::ostream& operator << (std::ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day << endl; return cout; } //会在main源文件和调用的源文件展开,会生成两个.o文件,main源文件和调用的源文件里面都有符号表的定义,链接的时候就会冲突 //所以全局函数不能在头文件中包含,不然可能会在多个源文件中定义,所以要定义在源文件 std::istream& operator >> (std::istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; }