目录
在c++中,结构体struct被认为是类的一种,
1.类名就是类型
2.可以在类里面定义函数
struct Stack { Stack* a; void a() { } };
class NN//NN是类名,class是定义类的关键字 { //类的主体,包含类的成员 //变量称为类的属性或成员变量 //函数称为类的方法或成员函数 }; class NN { void a(int h) { //... } int c; int k; }; 这样写,里面的函数,会被编译器认为是内联函数 第二种,可以把声明留在类里,定义放在外面或别的文件 比如类的声明在.h文件,定义在.c文件 .h文件 class NN { void a(int h); int c; int k; }; .c文件 void NN::a(int h) { //... } 注意,这里要类名:: 因为要用作用域限定符来指定函数究竟是哪个作用域的 注意,不要出现函数的形参和类里别的变量名或函数名相等的情况,容易出错 也容易混淆,一般对要加前缀或后缀区分,在函数中,如果没有指定作用域,那么编译器对于作用域,优先搜索 局部,再去全局变量或静态变量里找,有指定优先找指定 两种方法可以混用,在类里面定义的函数,虽然被认为是内联,但同样的 编译器也可以选择忽略 因此,长的函数定义放在外面,短的函数直接写在类里面
3.2类作用域
::是作用域限定符,{}也是定义一个作用域,
for(int i=0;i<10;i++) { int a; } cout<<a; 我们这时候就会编译错误,因为a是在{}所规定的局部域里面 定义的,在外面就不能用了
?3.1类访问限定
c++封装的方式,就是通过类将对象的属性和方法放在一起,通过访问权限,选择性的让外部用户调用接口(方法或对象)
访问限定分为:public公有,protected(保护),private(私有)
注意:
1.public修饰的成员,可以在外面直接被调用2.protected和private是不能在外面直接调用的(在这里pritected和private是一样的)
//注意,public在外面和类里面都能调用
3.访问限定符的作用域,直到遇到下一个访问限定符
4.如果没有遇到下一个,那就遇到类的}结束
5.class默认都是private,struct默认都是public(兼容c)
注意限定符是在我们写代码的时候限定我们调用的,在编译器编译之后,没有区别
class NN { public: void a(int h) { //... } private: int c; int k; };
4.1实例化
类本身就像是个设计图,在没有实例化前,是不占空间的
比如
class NN { public: void a(int h) { //... } private: int c; int k; }; int main() { NN c; c.a(3); return 0; } 必须先NN c先,才能对c这个实例化后的对象,进行访问其内部成员
4.2类对象大小
计算类大小的时候,不计算成员函数的大小,因为函数是放在函数表里的,所以在类里面是通过地址找函数的,所以一个类大小,只计算成员变量.
如果这个类是空(没有成员变量)的,没有成员,那就是1个字节,不存数据,就标识有这个对象存在过
计算类大小的时候可以参考结构体的内存对齐规则,具体可以看我结构体的文章
?
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout <<_year<< "-" <<_month << "-"<< _day <<endl; } } int main() { Date d1, d2; d1.Init(2022,1,11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; } 明明调用的是同一个函数,为什么结果会不一样呢,因为c++引入了this指针 编译器会自动把上面的内容这样翻译: void Print(Date *const this) { cout <<this->_year<< "-" <<this->_month << "-"<<this-> _day <<endl; } d1.Print(&d1); d2.Print(&d2); 注意,是编译器可以这样翻译,我们只能理解,不能直接这样写 形参和实参不能显示的使用this,只能在成员函数里面使用,比如上面cout的内容 就是合法的。
如果一个类,什么成员都没有,那么就称为空类,但事实上,并不是完全没有成员,类默认会有6个成员函数,构造函数、析构函数、拷贝构造、赋值重载、取地址重载(主要是普通对象和const对象取地址,很少会自己实现)
6.2构造函数
?构造函数是一个特殊的成员函数,名字与类相同,创建类类型对象时,编译器自动调佣,保证每个数据成员有合适的初始值,在对象的一个生命周期内只调用一次。简单来说,就是自动初始化对象的。
1.函数名与类名相同;
2.无返回值;
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
class NN { public: NN() {};//无参构造函数 NN(int c, int k)//带参构造函数 { _c = c; _k = k; } private: int _c; int _k; }; int main() { NN b;//调用无参构造,注意不能加(),否则就是函数声明了 NN c(3, 4);//调用带参构造函数 return 0; }
5.如果类里面没有定义显示的定义构造函数(就是我们自己写构造函数),那编译器会自动生成一个无参的默认构造函数,一旦我们显示定义了构造函数,那就不会生成。
如果我们只定义了带参构造函数,调用却是以无参构造函数的形式调用,那么就会报错
所以一般,我们用构造函数,都是定义一个带参(全缺省参数),这样就不会出事了,注意,不要让全缺省构造函数和无参构造函数同时出现,虽然构成了函数重载,但会出现歧义,还是会报错。
?6.编译器的默认构造函数,也是要分情况的。假如,我们用两个栈实现队列,这个时候,我们以无参的形式实例化队列,编译器会自动调用栈的默认构造函数(可能你自己定义了,也可能没定义让编译器自己生成)来初始化栈,但对于内置类型,也就是int\char\long long 等,理论上也会调用默认构造函数,但是这个默认构造函数什么都不干。(当然,如果你栈也是让编译器自己生成,那也是什么都不干)
注意,对于内置类型的默认构造函数,具体还是要看具体的编译器,vs2019有时候就会自动初始化,vs2013就不会。(注意,并不是说内置类型是个函数,而是说默认构造函数这个机制对于内置类型的处理,规定上是不处理,但一些编译器自己会优化)
7.针对上面的问题,c++11允许,内置类型成员变量可以在声明时就给默认值,但注意这还是声明,因为是放在类里的,也就是自定义类型,那就是个设计图,没有开辟实际空间,就还不是定义。
class NN { public: NN() {}; NN(int c, int k) { _c = c; _k = k; } private: int _c = 1; int _k; };
综上,一般情况下,我们都是自己写构造函数,但如果成员都是自定义类型(并且这些自定义类型我们都手动写了构造函数),可以让编译器自己生成,如果有内置类型,且在声明时就给了默认值,那也可以考虑让编译器自己生成默认构造函数。
8.默认构造函数只能有一个(无参构造函数、全缺省构造函数、我们自己不写,编译自己生成的构造函数,这3个之一,同时出现会报错)。
注意,整个实例化的类对象的销毁,是由编译器完成,那么对于对象里面的成员,如何销毁呢,如果只是成员变量或成员函数没什么,但如果是顺序表、栈这些复杂的东西,里面的空间是需要手动销毁的。
这个时候,对象在销毁时,会自动的调用析构函数,然后将对象中这些资源(空间等)的清理工作。
class NN { public: NN() {}; NN(int c, int k) { _c = c; _k = k; } ~NN() { _c = 0; _k = 1; } private: int _c = 1; int _k; }; int main() { NN b; NN c(3, 4); return 0; }
1.析构函数名是类名前+~。
2.无参数无返回值类型
3.一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数,注意,析构函数不能重载
4.对象生命周期结束时(要被销毁了),编译器会自动调用
5.默认生成的析构函数,对于内置类型,不做处理,也没必要处理,因为程序结束,操作系统会自动回收这些内置类型成员变量的空间的(类对象在实例化之后,对象的变量也是开辟在栈等空间上的,这些空间都是操作系统开辟的,回收也是由操作系统回收)。
对于自定义类型,则是会调用该自定义类型的默认析构函数(如果这个自定义类型没有我们主动写析构函数,那调用的也是该自定义类型的默认析构函数,只是大概率没效果)
举个例子,还是用两个栈实现队列,队列类的对象,编译器会自动生成一个对该队列的析构函数,队列的析构函数,会调用栈的析构函数,从而把两个栈都销毁掉(如果栈类的析构函数是我们自己写的话),因为没有别的自定义类型了,队列的析构函数就完成了任务。
如果已经存在该类的对象了,创建同类类型的新对象时,编译器会自动调用拷贝构造函数
?1.拷贝构造函数是构造函数的一个重载形式,一般得加const,以免拷贝错了
NN(const NN &a) { //... }
2.拷贝构造函数的参数只有1个,且必须是该类类型对象的引用(指针也行,但c++规定是引用),因为如果用传值传参,这个时候,拷贝构造函数的形参是接受来自实参的值传递,又相当于调用了一次新的拷贝构造函数,如此,无穷尽也,就会出事,所以,编译器会直接报错。
3.编译器自动生成的拷贝构造函数,对于内置类型,是值拷贝(浅拷贝),或者说,按内存顺序直接拷贝过去,对于自定义类型,会调用该类型的拷贝构造函数,为什么要独特处理呢。
注意,在c++中如果用值拷贝自定义类型,会出事,假如是栈(数据结构),那么用传值调用,由于析构函数在对象生命周期结束后,会自动调用,这个时候,形参和实参都会对空间进行一次释放。
因此对于涉及资源申请(例如空间开辟)的类,我们应当自己写拷贝构造函数,对于不涉及的,可以让编译器自己生成。(注意,还有一种类型,就是我们进行嵌套,比如用两个栈实现队列,这个时候队列的拷贝构造我们没必要写,因为对于自定义类型,会调用它的拷贝构造,所以我们只需要把栈的拷贝构造写好就行)
4.针对浅拷贝的问题,我们可以采用深拷贝,深拷贝就是开辟一样大的空间,一样的值,还是栈的问题,我们可以在拷贝构造函数里面,开辟跟被拷贝的栈一样大的空间,再把值拷贝到这个新开辟的空间,这样析构函数就算被调用了,也是各自释放各自的空间,也就不会出事了。
5.对于,拷贝构造,一般用在:用已经存在的同类的对象初始化或拷贝。
函数参数为类类型对象或返回值为类类型对象。
已存在类型,和函数参数前面说过了,这边主要是说返回值的问题,我们知道,传值返回的时候,因为函数调用结束,空间会被销毁,空间里的东西也都变成非法的,要创建临时变量来存储返回值,这个行为,是将类类型对象拷贝到临时变量,也要调用一次拷贝构造函数,之后把临时变量拷贝给接受的对象,那又会调用一次拷贝构造函数,所以对于返回值,我们可以采用引用返回的方式。
?6.4.1运算符重载
为什么要有运算符重载呢,因为我们有了类,这时候可能会需要比较,但问题是,编译器也不知道我们比较的规则等,所以编译器对于自定义类型,干脆是不提供比较方法,要我们自己想办法,对于内置类型,编译器最开始就知道怎么比,所以有运算符直接提供给我们用。
这些运算符可以直接转换为机器能识别的指令。
格式:返回值类型 operator操作符(参数列表)
bool operator> (NN x, NN y) { if (x._c > y._c)return true; else return false; } int main() { NN B1, B2; cout << (B1 > B2); //这个等同于cout<<(operator>(B1,B2)); return 0; }
但这样我们忽略了封装问题,成员变量一般都是利用,如果不采用全局的operator,我们可以封装成成员函数。
class NN { public: bool operator> (NN y)//可以是引用也可以传值 { if (_c > y._c)return true; else return false; } //对于成员函数来说,可以随意调用同类的成员,不受限制 //对于成员函数来说,一直有个默认的this指针,作为第一参数, //所以只需要一个参数即可. private: int _c = 1; int _k; }; int main() { NN B1, B2; cout << (B1 > B2); //B1>B2在编译器视角下,是NN::operator>(&B1,B2); return 0; }
运算符重载注意事项:
不能连接其他符号来创建新的操作符:比如operator$;
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数第一个参数为隐形的this指针。
.*? ? ::? ? sizeof? ? ?:? ? .? ?这5个不能重载
示例
class NN { public: NN(int year, int month, int day) { _year = year; _month = month; _day = day; } int getday(int year, int month1) { int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month1 == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)) { return 29; } return month[month1]; } //判断当前月份天数 NN& operator+= (int day) { _day += day; while (_day > getday(_year, _month)) { _day -= getday(_year, _month); _month++; if (_month > 12) { _year++; _month = 1; } } return *this; } //得出当前天数 NN operator+ (int day) { NN T(*this); T._day += day; /*while (_day > getday(_year, _month)) { _day -= getday(_year, _month); _month++; if (_month > 12) { _year++; _month = 1; } }*/ //前面已经有+=的操作了,我们这里可以省略,直接调用就行 return T; } //+的话,不应该改变自身,所以用拷贝函数创建一个新的对象。 void print() { cout << _year << '/' << _month << '/' << _day; } private: int _year; int _month; int _day; }; int main() { NN d1(2024, 1, 4); NN d2 =d1 + 100; d2.print(); d1.print(); return 0; }
6.4.2赋值运算符重载
格式:参数类型:const 类型名&,传引用提高效率
返回值类型:类型名&,返回引用提高效率,有返回值,是为了能够直接拿来赋值
检测自己是否给自己赋值
返回*this:返回值
示例
#include<iostream> using namespace std; class NN { public: NN(int year, int month, int day) { _year = year; _month = month; _day = day; } NN& operator=(const NN& a) { if (this != &a) { _year = a._year; _month = a._month; _day = a._day; } return *this; } void print() { cout << _year << '/' << _month << '/' << _day; } private: int _year; int _month; int _day; }; int main() { NN d1(2024, 1, 4); NN d2 =d1 + 100; d2.print(); d1.print(); return 0; }
1.赋值运算符只能重载成类的成员函数,不能重载成全局函数,因为编译器会自动生产一个赋值运算符的重载在类里面(如果你不显示定义),这样全局和类里面会冲突,所以不能重载全局。
2.编译器默认生成的赋值运算符,跟拷贝构造很像,对内置类型的变量,直接采用逐字节值覆盖,自定义类型采用该类型的赋值运算符。
3.同样,针对涉及资源管理(在堆上开辟空间等行为),最好是自己写赋值运算符,不涉及的,可以让编译器自己生成。
6.4.3前置++和后置++
NN& operator++() { _day++; return *this; } 前置++,当前类的day++,返回this指针 NN operator++(int) { NN tmp(*this); _day++; return tmp; } 为了区分前置和后置,c++规定,后置的里面参数要加个int,不用我们传,编译器自动传
?date.h
#pragma once #include<iostream> using namespace std; class NN { public: NN(int year=1, int month=1, int day=1); int getday(int year, int month1); NN& operator+= (int day); NN operator+ (int day); NN& operator=(const NN& a); NN& operator-= (int day); NN operator- (int day); NN& operator++(); NN operator++(int); NN& operator--(); NN operator--(int); bool operator==(const NN& a); bool operator>(const NN& a); bool operator<(const NN& a); bool operator>=(const NN& a); bool operator<=(const NN& a); bool operator!=(const NN& a); int operator-(const NN& a); void print(); private: int _year; int _month; int _day; };
date.cpp
#include"date.h" NN::NN(int year, int month, int day) { _year = year; _month = month; _day = day; if (_year < 1 || _month>12 || _day < 1 || _day>getday(_year, _month)) { this->print(); cout << "日期非法" << endl; } } int NN::getday(int year, int month1) { int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month1 == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)) { return 29; } return month[month1]; } NN& NN::operator+= (int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > getday(_year, _month)) { _day -= getday(_year, _month); _month++; if (_month > 12) { _year++; _month = 1; } } //* this = *this + day; return *this; } NN NN::operator+ (int day) { NN T(*this); T += day; //T._day += day; //while (T._day > getday(T._year, T._month)) //{ // T._day -= getday(T._year, T._month); // T._month++; // if (T._month > 12) // { // T._year++; // T._month = 1; // } //} return T; } NN& NN::operator=(const NN& a) { if (this != &a) { _year = a._year; _month = a._month; _day = a._day; } return *this; } NN& NN::operator++() { *this += 1; return *this; } NN NN::operator++(int) { NN tmp(*this); *this += 1; return tmp; } bool NN::operator==(const NN& a) { if (_year == a._year && _month == a._month && _day == a._day) { return true; } else { return false; } } bool NN::operator>(const NN& a) { if (_year > a._year) { return true; } else if (_year == a._year && _month > a._month) { return true; } else if (_year == a._year && _month == a._month && _day > a._day) { return true; } else return false; } void NN::print() { cout << _year << '/' << _month << '/' << _day; } bool NN::operator>=(const NN& a) { return *this > a || *this == a; } bool NN::operator<(const NN& a) { return !(*this >= a); } bool NN::operator<=(const NN& a) { return !(*this > a); } bool NN::operator!=(const NN& a) { return !(*this == a); } NN& NN::operator-= (int day) { if (day < 0) { return *this += (-day); } _day -= day; while (_day <= 0) { --_month; if (_month == 0) { --_year; _month = 12; } _day += getday(_year, _month); } return *this; } NN NN::operator- (int day) { NN a(*this); a -= day; return a; } int NN::operator-(const NN& a) { int flag = 1; NN max(*this); NN min(a); if (*this < a) { max = a; min = *this; flag = -1; } int d = 0; while (min != max) { ++min; d++; } return d * flag; } NN& NN::operator--() { *this -= 1; return *this; } NN NN::operator--(int) { NN tmp(*this); *this -= 1; return tmp; }
7.1>>和<<重载
注意,我们打印自定义类型不能直接用cout,为什么(我们之前知道cout和cin是自动识别类型,这是因为c++自己写了内置类型的函数重载,从而识别)cout是自ostream类里面的,cin是在istream类里面的。如果我们要用cout和cin输入输出指定的自定义类型,我们要运算符重载。
class NN { public: NN(int year=1, int month=1, int day=1); int getday(int year, int month1); NN& operator+= (int day); NN operator+ (int day); NN& operator=(const NN& a); NN& operator-= (int day); NN operator- (int day); NN& operator++(); NN operator++(int); NN& operator--(); NN operator--(int); bool operator==(const NN& a); bool operator>(const NN& a); bool operator<(const NN& a); bool operator>=(const NN& a); bool operator<=(const NN& a); bool operator!=(const NN& a); int operator-(const NN& a); void print(); friend ostream& operator<<(ostream& out, const NN& d); friend istream& operator>>(istream& in, NN& d); 这是友元,就是为了让外面的函数可以调用类私有的成员 private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const NN& d); istream& operator>>(istream& in, NN& d);
ostream& operator<<(ostream& out, const NN & d) { //这是流提取的运算符重载 //注意,我们不采用成员函数的形式重载运算符,而是全局 //因为,双目操作符的规定,第一个参数一定是左操作数,第二个参数是右操作数 //如果我们用成员函数的形式重载运算符,则会出现d1<<cout这样别扭的形式 //为了遵从习惯,我们采用全局的方式, //这样才能出现cout<<d1这样的形式。 // 注意,cout是个函数,需要被传参数,实现效果,不能 //加const,但要输出的类对象,我们可以用const修饰 //为了实现连续操作比如cout<<d1<<d2,我们要给函数一个返回值,返回cout,从而 //让cout继续做新一轮的左操作数,<<这个操作符的结合性是从左边开始的,赋值 //操作符的结合性是从右开始。out是cout的引用,返回out的引用,本质还是返回cout的引用,不影响 out << d._year << d._month << d._day << endl; return out; } istream& operator>>(istream& in, NN& d) { //跟<<差不多,区别是cin是在istream类里,且这次是流插入,也就是把东西 //插入类对象里面,所以类对象形参不能加const。 in >> d._year >> d._month >> d._day; return in; }
注意,重载>>和<<,一定是重载了流插入和流提取。因为位运算,一般是针对整型这样的内置类型。
void print() const 当这个成员函数,采用const修饰 当调用时实际上是这样翻译的 void print(const NN*const this) const NN d1; d1.print() 翻译:d1.print(&d1) 如果不加const,则是print(NN *const this) 这样我们传d1(用const NN d1),就会出现权限放大。 注意,如果我们用非const调用const修饰的print 不会报错,因为权限可以缩小。 因此,const对象和非const对象都可以调用const修饰的成员函数 但,const对象不能调用没有const修饰的成员函数
注意,为了保持参数匹配,事实上我们前面日期类函数,当有const对象参与,有可能会出问题,比如d1(const)<d2(非const),参数不匹配,所以,我们定义成员函数的时候,对于不会修改内容的函数,尽量都加入const修饰,会修改内容的成员函数,不要加const修饰。
?注意,const修饰的函数,如果有重名,本身也构成函数重载,因为接受的参数类型不一样
NN* operator&() { return this; } const NN* operator&()const { return this; } 注意,这两个重载,一般不会自己写,除非你想让别人获取到你指定的内容 自己不写,编译器自己会写。
这里我们要注意,编译器虽然会翻译我们的指令,但是不是像我在注释里写的那样,把我们的代码改掉,而是直接将代码翻译成正确的汇编指令,因此这里调用 重载的&在汇编指令的角度,不会造成死循环。具体可以看看汇编指令,这里不多说。
const类型的变量和引用类型的变量,都不能直接通过在构造函数里面初始化。
这里我们要讲明的是,类本身是个图纸,里面的成员变量只是声明,定义是在实例化类,也就是创建了类对象的时候。
而类对象里面的成员定义,是在初始化列表里面的,而const和引用这两类必须在定义的时候就初始化,所以,这两类必须在初始化列表里面定义并初始化。
而对于一般的成员变量(一般的内置类型),虽然在初始化列表里定义,但值是编译器给的随机值,所以我们有必要在构造函数里面给这些变量初始化。如果是自定义类型,会调用它的初始化列表进行定义,再调用它的构造函数.
class aa { public: aa(int a = 1) { h = a; } private: int h; }; class NN { public: NN(int year = 1, int month = 1, int day = 1) :a(year) ,c(_month) { _month = month; _year = year; _day = day; } private: const int a; int& c; int _year; int _month; int _day; aa h; }; 对于这个代码,自定义类型aa的h对象,会调用它的默认构造,而默认构造包含初始化列表和变量初始化。 因为这里的默认构造是缺省类型参数,所以编译器会自动初始化h 但如果是 aa(int a ) { h = a; } 那么,就必须 NN(int year = 1, int month = 1, int day = 1) :a(year) ,c(_month) ,h(2) { _month = month; _year = year; _day = day; } 来初始化
另外,对于一般的内置类型,如果我们给了缺省值,那么这个缺省值就是在初始化列表的时候就赋值给了变量。如果对于这样类型的变量,我们主动在初始化列表里赋值,那么就会经过(赋值缺省值-赋值给定值),如果是没有缺省值的,但我们给值,((这个不是初始化列表里初始化语句做的,可以结合下面的关于声明顺序的代码看)赋值随机值,赋值给定值),
借助初始化列表,如果遇到一个自定义类型a里面全是自定义类型b,且b类型的构造函数是一般参数,而不是全缺省参数,这时候,我们可以在a类型里面写构造函数,在初始化列表的部分初始化类型b。
总结下:
1.初始化只能初始化1次,因此一个成员在初始化列表只能出现一次
2.const类型成员变量,引用成员变量,自定义类型成员(注意,没有默认构造函数,主要针对是构造函数没有缺省值,必须我们主动给值)
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class aa { public: aa(int a) { h = a; } private: int h; }; class NN { public: NN(int year = 1, int month = 1, int day = 1,int n) :a(year) ,c(_month) ,h(n) { _month = month; _year = year; _day = day; } private: const int a; int& c; int _year; int _month; int _day; aa h; };
但不能全靠初始化列表搞定,比如判断空间开辟是否失败,以及memset置空等操作。
4.成员变量的初始化列表的初始化顺序,跟我们在里面写的先后顺序无关,而是与在声明时的顺序有关
class A { public: A(int a) :_a1(a) ,_a2(_a1) { } void Print() { cout<<_a1<<" "<<_a2<<endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); } 结果是1和随机值
为了防止我们有时候糊涂了,尽量声明顺序和初始化列表顺序一致
10.1explicit关键字
class NN { public: NN(int year = 1) { _year = year; } private: int _year; }; int main() { NN a(3); NN b = 4; } 4是个内置类型的 但赋值要匹配类型,所以这时候会出现隐式类型转换 这时候会创建一个临时变量NN c(4) 再用拷贝构造函数,把c拷贝给b 这样操作的前提是NN这个类的构造函数是单参数类型或者第一个参数没有 缺省,其他都是缺省值,又或者全缺省 注意,这样的话,浮点和整型都可以直接隐式转成类型NN 但像指针,就不行了,因为这里的隐式转换,本身还是将常量的类型匹配构造函数的第一个参数 指针无法隐式转int,所以NN b=0x100000是错的 但如果重载构造函数, NN(int *a) { /.... } 就可以了 class NN { public: NN(int year , int month = 1, int day = 1) { _month = month; _year = year; _day = day; c++; } NN(const NN& a) { _month = a._month; _year = a._year; _day = a._day; c++; } static int getc() { return c; } void h() { cout << getc(); } private: int _year; int _month; int _day; static int c; }; int main() { NN d1(2023,12,12) NN d2=(2023,12,12); 注意,这样两者表达是不同的,第一个是构造函数 第二个是NN d2=12,是逗号表达式。 如果是c++98是不支持多参数直接隐式转换的,但c++11支持 NN d2={2023,12,12}; const NN&d={2023,12,12}; }
如果不想支持常量隐式转换成类型
explicit NN(int year = 1) { _year = year; } 这样就能拒绝隐式转换,当然强转还是不能拒绝的
?类成员在声明时,有static修饰的,称为类的静态成员,静态成员变量,静态成员函数,静态成员变量一定要在类外面初始化。
静态成员变量不支持缺省值,因为定义和初始化不走初始化列表
1.静态成员为所有类对象共享,不属于具体哪个对象,放在静态区。
2.静态成员变量必须在类外定义,定义时不加static关键字,类里面只是声明
3.静态成员函数没有this指针,不能访问非静态成员
4.静态成员也是类的成员,收public、protected、private访问限定符的限制。
class NN { public: NN(int year = 1, int month = 1, int day = 1) { _month = month; _year = year; _day = day; c++; } NN(const NN& a) { _month = a._month; _year = a._year; _day = a._day; c++; } int getc() { return c; } private: int _year; int _month; int _day; static int c; }; int NN::c = 0; int main() { //NN g; //g.c=1 这里会报错,因为c是私有,改成公有就可以 //NN::c = 1; 如果是c是公有可以 //针对私有: //1种,为了调用而创建一个对象,并且加一个成员函数,返回c即可 //NN g //g.getc()-1 //创建一个匿名对象 //NN().getc()-1 //匿名对象生命周期只有这一行 //之所以-1,是因为这里c是用来计算类对象在创建时经过多少次 //构造和拷贝构造 }
class NN { public: NN(int year = 1, int month = 1, int day = 1) { _month = month; _year = year; _day = day; c++; } NN(const NN& a) { _month = a._month; _year = a._year; _day = a._day; c++; } static int getc() { return c; } private: int _year; int _month; int _day; static int c; }; int NN::c = 0; int main() { //此时getc函数是静态成员函数,不需要this指针 因此没有必要创建一个类对象 //直接用域限定符,就可以了. //注意,编译器查找,需要指向区域。 cout<<NN::getc() - 1; cout << NN().getc() - 1; NN a; cout << a.getc() - 2; }
静态成员函数,不可以调用非静态成员,因为没有this指针,无法指定是哪个对象的
非静态成员函数可以调用静态成员,因为有this指针,可以指定对象。
?12.1友元函数
class NN { public: NN(int year = 1, int month = 1, int day = 1); int getday(int year, int month1); NN& operator+= (int day); NN operator+ (int day); NN& operator=(const NN& a); NN& operator-= (int day); NN operator- (int day); NN& operator++(); NN operator++(int); NN& operator--(); NN operator--(int); bool operator==(const NN& a); bool operator>(const NN& a); bool operator<(const NN& a); bool operator>=(const NN& a); bool operator<=(const NN& a); bool operator!=(const NN& a); int operator-(const NN& a); void print(); friend ostream& operator<<(ostream& out, const NN& d); friend istream& operator>>(istream& in, NN& d); private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const NN& d); istream& operator>>(istream& in, NN& d); ostream& operator<<(ostream& out, const NN& d) { out << d._year << d._month << d._day << endl; return out; } istream& operator>>(istream& in, NN& d) { in >> d._year >> d._month >> d._day; return in; }
友元的关键字是friend
1.友元函数可以访问类的私有和保护成员,但不是类的成员函数
2.友元函数不支持const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同12.2友元类
class NN { friend OO; public: NN(int year = 1) { _year = year; } private: int _year; }; class OO { public: OO() { } int get() { c._year = 10; } private: int a; NN c; }; int main() { }
1.友元关系是单向的,不具有交换性。
上面代码,OO类里面可以访问NN类对象的私有成员,但NN类里面不能访问OO类的私有成员
2.友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3.友元关系不能继承。
?
#define _CRT_SECURE_NO_WARNINGS 1 #include "test.h" class OO { public: OO() { } class NN { public: NN(int year = 1) { _year = year; } private: int _year; }; private: int a; }; class LL { public: LL(int year = 1) { _year = year; } private: int _year; }; class KK { public: KK() { } private: int a; LL o; }; int main() { cout<<sizeof(OO); //答案是4,因为本质上没有定义NN类对象 cout<<sizeof(KK); //答案是8,因为KK类的声明里面有一个LL类对象 OO l; OO::NN u; //内部类在本质上还是一个类,比如NN类受OO类的域和限定符限制 //NN类本身天生就是OO类的友元 }
如果一个类定义在另一个类的内部,这个类就叫内部类。内部类是独立的雷,不属于外部类,不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类,内部类可以通过外部类的的对象参数,来访问外部类中的所有成员,但外部列不是内部列的友元。
1.内部类可以定义在外部类的public、protected、private都是可以的。
2.注意内部类可以直接访问外部类的static成员,不需要外部类的对象/类名
3.sizeof(外部类)=外部类
?14.匿名对象
class NN { public: NN(int year = 1, int month = 1, int day = 1) { _month = month; _year = year; _day = day; c++; } NN(const NN& a) { _month = a._month; _year = a._year; _day = a._day; c++; } int getc() { return c; } private: int _year; int _month; int _day; static int c; }; int NN::c = 0; int main() { //创建一个匿名对象 NN().getc()-1 //匿名对象生命周期只有这一行 //之所以-1,是因为这里c是用来计算类对象在创建时经过多少次 //构造和拷贝构造 匿名对象不用取名,生命周期只有一行,结束后自动调用析构函数 }
?
#define _CRT_SECURE_NO_WARNINGS 1 #include "test.h" class NN { public: NN(int b=1) { a = b; } private: int a; }; int main() { NN a(1); NN b = a;//这是拷贝构造,因为b是还未定义的,a是已经定义的 NN c(2); c = a;//赋值拷贝,因为两者都是已经初始化过的,这里是赋值。 //按照c++的语法,按理来说NN c=1,要先构造临时变量,再用临时变量拷贝构造c //但编译器对于这个行为,进行了优化,在实际上只会进行一次构造。 //在同一个表达式中,有些行为会被合二为一。构造+构造-》构造 //构造+拷贝构造-》构造 //拷贝构造+拷贝构造-》拷贝构造 //上面是一般编译器都会做的优化,但一些编译器会非常激进,把中间不必要的东西直接优化了 }
?