开局之前,先来个小插曲,放松一下:
让我们的熊二来消灭所有bug
各位,在这祝我们:
放松过后,开始步入正轨吧。爱学习的铁子们:
目录:
一·类的定义:
1.简述:
2.访问限定符:
3.类域:
二·实例化及类的对象大小:
三·this指针的简述:
四·类的默认成员函数:
1.构造函数:
2.析构函数:
3·拷贝构造函数:
4.运算符重载:
4.1赋值运算符重载:
4.2运算符重载实现日期类计算器:
4.3取地址运算符重载:
五·额外补充:
5.1初始化列表:
5.2 类型转换:
5.3 static 成员:
5.4 友元:
5.5内部类:
5.6匿名对象:
5.7对象拷⻉时的编译器优化:
一·类的定义:
1.简述:
朴素点来说类就是在c++中对c中的结构体(struct)的优化,升级成了class。
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省 略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的⽅法或 者成员函数。
C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
为了区分类中的成员变量和函数传参的量,通常在成员变量前或后加上_。
对类里面的函数一般默认是inline修饰的内联函数。
2.访问限定符:
可以分为 public,private,protected。可以理解为public在类中属于公有的外界也可以访问,但是后两者大致可以认为私有的,只有类体内自己可以访问到,在外界没有权限访问。
使用一般遵循这几点:
①访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有 访问限定符,作⽤域就到}即类结束。
②class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
③⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
3.类域:
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤类名+::指明成员属于哪个类域。
这样就定义好一个简单的类了。
class date {
public:void init() {_year = 2024;_month =7;_day = 10;}void print() {cout << _year << "/" << _month << "/" << _day << endl;
}
private:int _year;int _month;int _day;};
二·实例化及类的对象大小:
实例化:类里只是限定了有哪些成员/对象,并没有真实为它们开辟空间,故还需要外面调用初始化等操作来完成,即真实地创造类的实例化对象。
也可以简单的想象成类就是造房子的图纸,并不是真正的房子,还需要给它拿实物搭建,故类的实例化就像拿图纸造房子一样。
所谓对象大小可以简单理解为,把类看成c语言中定义的结构体,即它的每个对象都占有一定的字节大小,而类似结构体对齐的方法可以确定类的大小。
顺便说一句,为什么要结构体对齐要存在空出多余的呢?:这是由于cpu读取内存会从某个固定整数倍读取,如果空出来的话,可以提高cpu速率,而直接挨着排,可能一次性不能读完整,故还需要整合,即这里采用的是对齐方法。
三·this指针的简述:
this就是在类的对象函数的形参形成的时候会有一个隐式的类的指针变量,类型就是类的名字。
注意:①在类中完成查询某个类体的成员,赋值等操作都是通过它完成的。
②在函数的实参形参,不能出现this而编译器默认已经加上了,但是函数内容里面可以用。
③this指针作为函数形参随函数而动,故存在于内存栈区。
void init() {this->_year = 2024;this->_month =7;this->_day = 10;}
四·类的默认成员函数:
1.构造函数:
可以理解为对类的初始化的函数。
特点:①函数名字与类名相同故写的时候只用写一个类名(如:date(){})。
②无返回值无返回类型。
③可以进行重载。
④可以分为全有参,全缺省,无参,半却省等,当如果调用的是无参函数或者是不想输入参数的全缺省故无需();如:date d1。
⑤默认构造函数:分为无参构造函数,全缺省构造函数,编译器默认生成的无参构造函数,三者只能出现一个。
总结:一般构造函数都是自己操作完成,很少编译器自己完成,比如栈stack就需要由于开辟了空间和赋值,这时就需要自己完成构造函数,而对于两个栈合成的队列,这时候在队列中让它自己调用栈完成即可,故无需自己构造函数。
class date {
public:void init() {this->_year = 2024;this->_month =7;this->_day = 10;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}//如果外部定义的p为空指针,而这两个函数执行的时候要有解引用操作,故崩溃//全省的构造:/*date(int year = 2024, int month = 7, int day = 10) {_year = year;_month = month;_day = day;}*///全参构造:/*date(int year, int month, int day) {_year = year;_month = month;_day = day;}*///无参构造:/*date() {_year = 2024;_month = 7;_day = 10;}*///半省函数:date(int year, int month, int day=10) {_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};int main()
{date d1;//对编译器自己生成的或者无参的调用d1.init();d1.print();date d1(2024,7);//半缺省的调用d1.print();
return 0;
}
后面在析构函数内结合构造函数对栈,队列的构建结合。
2.析构函数:
大致就是对已经初始化的对象所带来的空间资源等进行销毁操作。
特点:①类似构造函数,但是它都是无参的,调用也同无参的构造函数同,定义是在名前加~(如~date(){})。
②无参数无返回值。
③生命周期快结束时候自动调用。
④编译器可自己完成析构并自动调用。
⑤一般有资源申请的时候一定要写析构,否则出现资源泄露,没有的时候可以不写,如:stack类一定要写,而myqueue可以不写,下面会提到。
从此可以看出c++在初始化和销毁做到了对c的优化。
~stack() {//程序结束后自动调用free(_a);_a = nullptr;_top = _capacity = 0;if (_a == nullptr) {cout << "_a空" << endl;}}
构造函数和析构函数结合:
class stack {
public:stack(int n=4){_a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;if (_a != nullptr) {cout << "a非空" << endl;}}~stack() {//程序结束后自动调用free(_a);_a = nullptr;_top = _capacity = 0;if (_a == nullptr) {cout << "_a空" << endl;}}private:size_t _capacity;size_t _top;int* _a;
};class MyQueue
{
public://由于是有两个栈类合成的队列,故可以省略构造和析构,到最后还是会调用栈类,从这个里面//构造和析构。
private:stack pushst;stack popst;int size;
};int main() {MyQueue mq;return 0;
}
对于这两个栈类合成的队列:队列既没有初始化即没写构造函数,又没写析构函数,而都靠的是它的类的成员对象自己调用的自己栈类里面的构造和析构。
3·拷贝构造函数:
简单就是一种特殊的构造函数,但是它要求第一个参数必须是自身引用类型,剩下的也可以有参数,但必须有默认值。
为什么要是这样?
Date(const Date& d){_year = d._year; _month = d._month;_day = d._day;}Date(const Date d){_year = d._year; _month = d._month;_day = d._day;}int main(){Date d1;
Date d2(d1);//如果调用第二个,由于是传参,首先要调用拷贝构造,这样会一直循环调用下去,而第一个是引用,直接就是d1故不用这样。
return 0;
}
①这里如果是调用拷贝构造有两种写法:date d2(d1); date d2=d1; 这样中一样,故可以选一种。
注:在c++中规定对类 类型的传值传参都要先调用它 的拷贝构造函数,而引用无需。
class date {
public:void init() {this->_year = 2024;this->_month = 7;this->_day = 12;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}date(int year, int month, int day) {_year = year;_month = month;_day = day;}
//拷贝构造函数:
date(const date&d) {_year = d._year;_month = d._month;_day = d._day;//这里也可以不写拷贝构造,无资源的发生,编译器可以自己调用
}private:int _year;int _month;int _day;};int main() {date d1;//自己调用拷贝构造函数两种方式://date d2(d1);//date d2 = d1;//d2.print();date d2(2024, 7, 13);d1.func(d2);//d2调用拷贝构造函数:date dc=d2;然后把dc进入func完成操作。d1.print();return 0;
}
②拷贝构造函数它是构造函数的一个重载。
③对于编译器自己生成的拷贝构造都是浅拷贝(一个一个字节的拷贝)(如有的地方会开辟空间,有资源的时候不适用,因为这样会使它们的地址相同,然后会析构两次,程序就会崩溃)因此,需要自己来写深拷贝来完成如stack类就需要自己开辟空间释放等。
下面是一段stack和两个stack合成的Myqueue类代码:
class stack {
public:stack(int n = 4) { _a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;cout << "Stack()" << endl;}~stack() {//程序结束后自动调用cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}//栈的拷贝构造:假想在要拷贝的类的对象里面完成的把st拷贝给这个对象stack(const stack& st)//有空间的开辟,要自己写拷贝构造{cout << "stack(const stack& st)" << endl;_a = (int*)malloc(sizeof(int) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(int) * st._top);_top = st._top;_capacity = st._capacity;}void push(int x)//这里由于参数不是类的类型故调用push函数的时候不需要自己掉拷贝构造{if (_top == _capacity){int newcapacity = _capacity * 2;int* tmp = (int*)realloc(_a, newcapacity *sizeof(int));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}private:size_t _capacity;size_t _top;int* _a;
};class MyQueue
{
public:private:stack pushst;stack popst;int size;
};
当把1推进s1,并且拷贝给s2(此时不注释掉自己写的拷贝构造):
int main() {stack s1;s1.push(1);
stack s2(s1);
return 0;}
第一个打印是首先对s1的构造函数进行的初始化;接着把1推进s1,第二个打印是把s1通过拷贝构造给s2,接着后面两次就是当快结束程序的时候遵循类似栈的后进先销毁的模式,先对s2,再对s1销毁 。
当如果把自己写的拷贝函数注释掉,再次重复会发生什么呢?
可以看到程序崩溃了。
那为什么呢?
int main() {stack s1;s1.push(1);stack s2(s1); //由于为写拷贝构造函数,编译器用自己默认的,即浅拷贝,这时候地址拷贝的s1与s2就是相同的,那么当s2这块空间被free掉后,s1又会对同一块被销毁的区域再次销毁,自然就崩溃了。return 0; }
对Myqueue无需写构造,拷贝构造,析构,一切都交给了它的成员变量stack了。
int main() {MyQueue mq;MyQueue mmq(mq);return 0; }
对Myqueue的分析:
这个类里面都是编译器默认生成的默认成员函数;故每次都是先对它的成员变量的操作,即进入Myqueue的默认函数然后再次跳转到对stack的一系列构造;拷贝构造以及析构等,靠stack来对它完成。
④传值和传引用的返回区别:引用返回会直接返回,而传值返回最后会对结果拷贝一下。就是区分返回类型是否被引用了,被引用的话则要保证对象出了函数作用域不被销毁。如:
stack &func(){
stack st;
return st;
}
int main(){
stack ret=func();
return 0;
}//如果这样操作的话直接返回的就是指向函数对st操作的区域,当函数被销毁后,将会出现野指针随便指向区域现象
此问题可以函数域内靠static固定在静态区解决。
对拷贝构造函数的总结:只要有资源产生或者销毁方面,就要自己写拷贝构造函数来完成一系列操作。
4.运算符重载:
①运算符重载是具有特名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
②当对类体使用运算符的时候,必须有对应的运算符重载,即对应的函数,否则报错。
③比如比较大小,这个函数可以写在类内,也可以是类外:
类外:由于变量是private,则需要从类的public内写获取变量值返回函数。
/ bool operator==(date d1,date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() == d2.getmonth()
// && d1.getday() == d2.getday();
//}
// bool operator>(date d1, date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() >d2.getmonth()
// && d1.getday() == d2.getday();
// }//可以假设年和日都相同,比较月大小
// bool operator<(date d1, date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() < d2.getmonth()
// && d1.getday() == d2.getday();
// }
类内:(调用访问的时候是从一个类体的指针去访问这个函数,然后进入这个函数,由于传参是另一个类体,故得到指针,再得到它的对应的变量值与前者的变量值比较最后返回。因此如果是类内定义可以减少参数的量)
class date {
public:void init() {this->_year = 2024;this->_month =7;this->_day = 10;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}//由于私有外界无法访问故可以把它的值当做返回值于公有返回。int getyear() {return _year;}int getmonth() {return _month;}int getday() {return _day;}bool operator==(date d2) {return _year == d2._year &&_month == d2._month &&_day == d2._day;}private:int _year;int _month;int _day;};
主函数:
date d1(2024,7);date d2(2024,8);date d3(2024, 6);/*cout << operator==(d1, d2) << endl;cout << (d1==d2) << endl;//d1==d2会自动转换为上面的operator==(d1, d2) cout << operator>(d1, d2) << endl;cout << (d1 > d2) << endl;cout << operator<(d1, d2) << endl;cout << (d1 < d2) << endl;*/cout << d1.operator==(d2) << endl;cout << (d1==d2)<< endl;//指向d1的指针去访问:date* p = &d1;cout << p->operator==(d2) << endl;
④运算符重载后,它的优先级与结合性应与内置类型保持相同。
即当调用的是运算重载,但内外运算顺序不变。
⑤不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
⑥.*| ::| sizeof| ?:| . 注意以上5个运算符不能重载。
⑦如重载有前置++和后置++,应该有区分,后置++要求有一个int类型参数,又由于函数本身没用到它,故可以不用接收写成(int),而前置没有。
4.1赋值运算符重载:
属于一种深拷贝或者浅拷贝的一种类型;即有两个初始化完成的类对象,之间的赋值过程;
也许他会和拷贝构造联系但是拷贝构造是把一个对象的值给一个未出现的对象的初始化。
它的一些规定及特点:
①一般用const修饰,而且定义必须当成员函数。
②一般参数类型也要用引用,如果有返回值(比如连续赋值操作)返回类型也要用引用,提高了效率。
③未写此处,编译器也会默认生成,不过生成的就是浅拷贝了(参数无引用的话)。
④类似date类的,编译器也会自己实现赋值操作,可以不用写,而类似stack类的,存在资源空间就要自己写。
代码写法:
对自写的赋值运算符函数实现:
date &operator=(const date& d) {_year = d._year;_month = d._month;_day = d._day;return *this;
}//由于想要进行连续赋值操作,故这里返回类型引用一下并且让它能返回被赋值后的对象。
完整的class date:
class date {
public:void init() {this->_year = 2024;this->_month = 7;this->_day = 12;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}date(int year, int month, int day) {_year = year;_month = month;_day = day;}//拷贝构造函数:date(const date&d) {_year = d._year;_month = d._month;_day = d._day;}//被传参的函数:void func(date d) {d.print();cout << &d << endl;}date &operator=(const date& d) {_year = d._year;_month = d._month;_day = d._day;return *this;}
private:int _year;int _month;int _day;};
int main() {//date d1(2024, 7, 12);//date d2(2024, 7, 13);//date d3 (2024, 7, 11);//d3=d2 = d1;//编译器默认的浅拷贝,可以不用写//d2.print();//d3.print();date d1(2024, 7, 12);date d2(2024, 7, 13);date d3 (2024, 7, 11);//自行实现的赋值重载;可连赋d3.operator=(d2);d3.print();d3 = d2 = d1;d3.operator=(d2.operator=(d1));//两种写法等同return 0;
}
最后总结:只要显示了构造,析构等存在资源开辟及销毁;就要写拷贝构造和赋值重载。
4.2运算符重载实现日期类计算器:
下面是用运算符重载完成的一系列函数实现的日期类:
date.h:
#pragma once
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>class Date
{
public://防止当这两个函数在类里面的话会出现d1<<cout等情况,这里放在外面,可是不能用类里的成员变量//故用友好函数,告诉类,让它在外面可以用到friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}~Date() {_year = 0;_month =0 ;_day = 0;}bool CheckDate() {if (_month < 1 || _month > 12|| _day < 1 || _day > getday(_year, _month) || _year < 1){return false;}else{return true;}}void Print();int getday(int year, int month) {assert(month > 0 && month < 13);static int arr[13] = { 0,31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}return arr[month];}bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d)const;bool operator==(const Date& d) const;bool operator!=(const Date& d)const;Date operator+(int day)const;Date& operator+=(int day);Date& operator-=(int day);Date operator-(int day)const;//前后置++--:后置一般多一个int参数,而这个参数对函数没有故不用接收Date operator++(int);Date& operator++();Date operator--(int);Date& operator--();//类对象之间的相减int operator-(const Date& d);//取地址重载函数/*Date* operator&(){return (Date*)0xf2134214;}*//*const Date* operator&(int){return (Date*)0xf2134214;}*/private:int _year;int _month;int _day;
};//为了可以类似cout<<a<<b<<endl;:这里让它有返回值,防止返回拷贝故用了返回引用
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
date.cpp:
#include"date.h"void Date::Print()
{cout << _year << "/" << _month << "/" << _day << endl;
}//比如d1+=1003;
//这里要在+里面引用+=:/运算比较::Date& Date:: operator+=(int day) {if (day < 0) {return *this -= (-day);//判断输入的day是负数情况}_day += day;while (_day > getday(_year, _month)) {_day -= getday(_year, _month);_month++;if (_month > 12) {_year++;_month = 1;}}return *this;
}Date Date:: operator+(int day)const {Date tmp = *this;//对tmp操作:tmp.operator+=(day);return tmp;
}
//在-里引用-=://Date Date::operator-(int day)
//{
// Date tmp = *this;
// tmp._day -= day;
// while (tmp._day <= 0)
// {
// --tmp._month;
// if (tmp._month == 0)
// {
// tmp._month = 12;
// --tmp._year;
// }
//
// tmp._day += GetMonthDay(tmp._year, tmp._month);
// }
//
// return tmp;
//}
////Date& Date::operator-=(int day)
//{
// *this = *this - day;
//
// return *this;
//}
// 上面要拷贝三次不推荐
//下面的方式只要拷贝2次:
Date& Date::operator-=(int day) {if (day < 0) {return *this += (-day);//判断输入的day是负数情况}_day -= day;while (_day <= 0) {if (_month > 0)_month--;else {_year--;_month = 12;}_day += getday(_year, _month);}return *this;
}//保证要输入的对象不变
Date Date::operator-(int day)const {Date tmp = *this;tmp -= day;return tmp;
}bool Date::operator==(const Date& d)const
{return _year == d._year&& _month == d._month&& _day == d._day;
}
bool Date::operator<(const Date& d)const {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;}else {return false;}}else {return false;}}else {return false;}
}bool Date:: operator>(const Date& d)const {return !(*this <= d);
}bool Date::operator<=(const Date& d) const
{return (*this < d) || (*this == d);
}bool Date::operator>=(const Date& d) const {return (*this > d) || (*this == d);
}
bool Date::operator!=(const Date& d)const {return!(*this == d);
}//前后置++--:Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;//由于在函数里面有定义的tmp,出了函数销毁,故需要临走的时候需要拷贝故不用返回引用
}Date& Date::operator++()
{*this += 1;//这里里面没有定义,而是对this外部传进的直接操作,为了提高效率可以返回的时候引用return *this;
}Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}Date& Date::operator--()
{*this -= 1;return *this;
}<< >>的重载:ostream& operator<<(ostream& out, const Date& d) {out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;}
istream& operator>>(istream& in, Date& d) {int jud = 1;while (jud) {cout << "请输入日期<";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "输入日期非法:";d.Print();cout << "请重新输入!!!" << endl;}jud = 0;}return in;
}int Date::operator-(const Date& d) {//先用假设法假设好max与min,然后判断,不符合的话就再换Date max = *this;Date min = d;int n = 0;int flag = 1;if (min > *this) {min = *this;max = d;flag = -1;}while (min != max) {min++;n++;}return flag * n;}
通过它可以简单实现一些对日期的计算,类似于日期计算器:
网址链接:日期计算器
4.3取地址运算符重载:
首先先认识一下const成员函数:类内不希望被修改的成员函数可以在函数后加上const。
如:对于成员函数:
void print()const{...
}//这个参数就是this指针 类型:date const*this;而编译器已经不希望被修改this;但是可以修改它指向的对象,如果用const修饰这个成员函数,相当于对this指针操作:使它变成 const date const *this;这样的话,它指向的也不能改了。
//调用这个被const修饰的成员函数前先对外面的对象初始化:
const date d1//相当于权限平移date d1//相当于权限缩小
后面就是用到取地址运算符重载 :
即通过得到对象里面的地址;返回是类的类型;如果直接调用如&d1;编译器会自己生成地址运算符重载函数,可直接用;也可以自己在类里面写,但是注意:当写的比如改动地址想让它调用这个,必须要和编译器默认的函数互为重载才能调用。
写法:
class date{public:
date*operator&(){
return ...}private:
...
};
对于const对象取地址也就是限制了返回类型,是无法修改的,其他和普通对象取法一样。
默认成员函数写法总结:都是可以写在类里面的,而比如构造析构拷贝构造,一定要在类里面(如果分文件,如.h;.cpp;那就在.h里声明,.cpp里定义且要明确一下如:date::这样也相当于在类里) ;运算符重载函数除了赋值运算符要写在类里;其他都可,但是写在类里可以减少一个参数(this所指)。
五·额外补充:
5.1初始化列表:
①首先它是构造函数的一种方式;初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
如:
class A{public:A(int a):_a1(a),a2(_a1){}void print(){}private:int _a1=1;int _a2=1;
}
②初始化列表只能出现一次,因为这是成员定义的地方。
③对于声明的时候给的值是缺省值,也就是如果初始化列表没有这个成员变量,然后他会用缺省值。
④对有const类变量必须在初始化列表定义或者用缺省值,因为它无默认构造;对于引用也是必须有初始化即在初始化列表中定义。
⑤编译器是按照声明顺序去给变量定义的,故尽量把声明和定义顺序写一致。
初始化列表的步骤过程总结:编译器从private这里的声明处开始一步步往下走,<1>有对应的初始化列表就调用,<2>没有的话看看有无缺省值,a有就用,如果在没有,b就要靠默认构造了,b<1>对于内置类型,编译器生成的默认构造是随机值;b<2>而自定义类的类型如果自己没写默认构造将会报错。
下面是有关初始化列表调用的题:
#include<iostream>
using namespace std;
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {cout << _a1 << " " << _a2 << endl;}
private:int _a2 = 2;int _a1 = 2;
};
int main()
{A aa(1);aa.Print();
}
来看看这道题最后输出什么?
首先声明先遇到的是_a2,到对应的列表去找结果它的值是_a1,而_a1此时还未初始化,故是随机值;然后声明就走到了_a1,对应main函数中的1;故最后结果是 1和随机值。
5.2 类型转换:
即c++支持类内隐式类型转化为类的类型对象,然而类内应该有对应的输入类型的成员函数。
举例:
class A {...A(int a) {...}A(int a1, int a2) {...}void print() {...}...
};
int main() {A aa1 = 1;//简单理解为:1定义的一个类类型的对象拷贝给aa1。//具体过程:首先拿1构建一个类类型的临时对象然后再拷贝构造给aa1// (对于连续的构造及拷构,编译器优化成直接构造)aa1.print();const A& aa2 = 1;//由于出现了临时对象,表现常性,故用const接收确保它不能修改//c++11后优化了对两个参数的类型转化即:A aa3 = { 1,2 };return 0;
}
这样的话是不是看不到它的用途呢?下面看一下它的应用,可以简化代码步骤:
class A {...A(int a) {...}A(int a1, int a2) {...}void print() {...}...
};
class stack {
public:void push(const A &aa) {...}private:A_arr[10];int_top;
};
int main() {//第一种写法:stack st;st.push({ 2,2 });//第二种写法:stack st;A aa3(2,2);st.push(aa3);//明显利用了类型转换减少步骤。return 0;
}
故对类似这种类的嵌套等就可以用这样的类型转换减少步骤。
5.3 static 成员:
①静态变量属于类,不属于某个对象,而位于静态区,隶属全局;即类内声明,类外定义。
②如:类内:private: static int _a; 类外:int 类名:: _a=1;(这里外部访问静态量或函数必须打破类域即用类名:: )。
③用static 修饰的成员函数即静态函数,类似全局的,故无this指针,所以不能访问类内的其他成员,只能访问静态的而非静态函数既可访问静态也可以访问非静态。
④静态成员也是类的成员也受访问限定符的限定即如果放在private也是外部无法访问。
⑤静态成员的访问可以用类名+::或者对象+.来获取。
总结:static既有部分类的性质(如:受访问限定符限定,外部在private无法访问)又有全局的性质(如:定义要在类外面,无this指针,访问成员有权限等)。
例题:
求1+2+3+...+n_牛客题霸_牛客网
这里就是应用了static成员:
//思路:由于限制了一些判断;故用累加,可以多次调用类对象的初始化,而防止初始化每次改变成员变量
//的初始值,把成员变量static成在类中类似的全局变量,对它累加即可。
//class sum {
//public:
// sum() {
// _ret += _i;
// _i++;
// }
// static int re_ret() {
// return _ret;
// }
//private:
// static int _i;
// static int _ret;//当多次用构造函数初始化,而不希望改变一开始的值可以考虑用static
//};
//
//int sum::_i = 1;
//int sum::_ret = 0;//在类内声明,在类外定义,static专属的外部用要突破类域用类名+::
//
//class Solution {
//public:
// int Sum_Solution(int n) {
// // sum a[n];vs不支持变长数组:这里初始化了一个sum类型变长数组
// sum* p = new sum [n];//用new来开辟空间sum类型,类似malloc 后面是类型+数组大小
// delete[]p;
// return sum::re_ret();//static函数外部用亦是如此。
// }
//};
5.4 友元:
①友元分为友元函数和友元类:
②友元函数:从外部定义函数,类里面用friend+声明;就可以让这个函数通过比如对象+.形式去访问类成员。(可以在任意地方声明,不受访问限定符限定)
③一个函数可以是多个类的友元。(即可以访问多个类的成员)
④对于友元类:如果b是a的友元类,那么b就可以访问a的成员。(多侧重于a的私有的时候常用,即在b类里如函数存在a类的对象可以以此访问a的私有)如:
class A
{// 友元声明friend class B;
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:void func1(const A& aa){cout << aa._a1 << endl;cout << _b1 << endl;}void func2(const A& aa){cout << aa._a2 << endl;cout << _b2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};
int main()
{A aa;B bb;bb.func1(aa);bb.func1(aa);return 0;
}
⑤友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元; 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是B的友元。
⑥友元特征:有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
5.5内部类:
①即两个类的嵌套;即⼀个类定义在另⼀个类的内部;内部类是⼀个独⽴的类,跟定义在 全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
②内部类默认是外部类的友元类。
③如果一个a类设计出来就是为了只给b类用,那么可以考虑把a放入b的private;成为b的专属类,则只有b可以用,外部无法用。比如下面的例题。
总结:内部类相比于分开定义两个类区别:1.内部类会自动变成外部类的友好类;2.限定符限定和类域限定:a:即如果把内部类放到public则需要外部用外部类+::突破外部类域才能访问到,而对应外部类访问它就像类外访问一个类一样。b:而放到private或protected就只能外部类访问,类外无法访问。
对于上面那道static的题也可以用上内部类;由于设计出的sum类如果只是想给Solution用,其他外部无法调用可以把它放入私有,如:
//内部类应用:比如只让 Solution能用sum,放在它的私有
class Solution {
public:int Sum_Solution(int n) {// sum a[n];sum* p = new sum[n];return sum::re_ret();}private:static int _i;static int _ret;//让它少突破一层类域的限制class sum {public:sum() {_ret += _i;_i++;}static int re_ret() {return _ret;}};};
int Solution::_ret = 0;
int Solution::_i = 1;
5.6匿名对象:
①即用类型定义出的没有名字的对象。
如:
a();
a(1);
//以上都是匿名对象
//以下是有名对象
a a1;a a1(1);
//而不能这么定义:a a1()编译器无法识别是定义还是声明
②区别:匿名对象作用域只在当前这一行出了后立刻调用析构函数,而有名对象是当前整个域,出了后才析构,相比而言前者范围更小,故当临时定义对象的时候可以用匿名,而const可以延长它的生命周期。
5.7对象拷⻉时的编译器优化:
①现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参 过程中可以省略的拷⻉。
②如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译还会进⾏跨 ⾏跨表达式的合并优化。
下面举个简单的例子:
class A
{
public:A(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){...}A(const A& aa):_a1(aa._a1){...}
private:int _a1 = 1;int _a2 = 1;
};
int main(){A a1=1;cosnt A&a2=1;return 0;
}
解释:数字1是类型转换,本来是掉A默认构造函数,然后创建临时对象再默认拷贝给a1;这里优化直接省去了拷贝,直接临时对象给a1,a2也同理,直接让调用完的默认构造,直接拿a2引用这个对象。
这样类似甚至更多的编译器优化还有很多,比如还有直接把表达式转换给优化掉的,甚至更加离谱的,都取决于强大编译器的机制,感兴趣的可以去观察尝试一下。
以上就是个人对c++类和对象这一方面简单及其简化版的理解,外加补充一些生动简化的例子,希望有助于读者学习和理解。