文章目录
- 前言
- 一、构造深入
- 1.初始化列表
- 2.隐式类型转换
- 1.隐式类型转换
- 2.explicit
- 3.委托构造
- 二、类的静态成员
- 1.静态成员声明
- 2.静态成员定义
- 3.静态成员特性
- 三、重载运算符和类型转化
- 1.关系及算数运算符重载
- 2.递增递减运算符重载及如何区分
- 3.赋值运算符重载
- 4.重载输入输出运算符
- 1.重载输入运算符
- 2.重载输出运算符
- 5.函数匹配与重载运算符
- 四、类的其他细节
- 1.const成员函数
- 2.内部类
- 内部类的特性
- 3.匿名对象
- 4.拷贝时编译器的优化
- 5.对封装的进一步认识
- 总结
前言
对于任何C++的类来说,构造函数都是其中重要的组成部分,我们已经在上篇介绍了类的一些基础知识,这篇我们将继续了解类的一些其他功能。并对之前讲解过的内容(如构造函数)进行一些更加深入的讨论。
一、构造深入
1.初始化列表
有时我们可以忽略数据成员初始化和赋值的差异,但并非总是这样。如果成员是const或者是引用的话,我们必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员进行初始化。
class STU
{
public://错误,my_id和rid必须要被初始化STU(int i){id = i;//正确my_id = i;//错误:不可以给const赋值rid = i;//错误:rid没有被初始化}
private:int id;const int my_id;int& rid;
};
我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值!!!
STU(int i):id(i),my_id(i),rid(id){}//显示的初始化引用和const成员
上面就是我们的初始化列表。
初始化列表: 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 引用成员变量,const成员变量和自定义类型成员(且该类没有默认构造函数时)必须放在初始化列表位置进行初始化
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:A(int i):a(i),b(a){}void print(){cout << "A:" << a << " B:" << b << endl;}
private:int b;int a;
};
int main()
{A a(10);a.print();return 0;
}
上面代码的结果是什么样的呢?是10,10吗?
从运行结果可以看出,显然不是上面的10,10。为什么有随机值呢?
根据我们上面的第四条,我们先声明的b,初始列表会按照声明顺序进行赋值,和初始化列表中的先后次序无关,所以还是先对b进行赋值,b的值来自a,此时a为随机值,所以b现在也为随机值,为b赋值后轮到a,此时a的值来自i,所以a是10.
2.隐式类型转换
1.隐式类型转换
在C++中,我们的内置类型存在隐式转换,同样的,我们的类存在这样的隐式转换。如果构造函数只接受一个实参,则它实际上定义了此类型的隐式转换机制。有时我们把这种构造函数称为转换构造函数。
class A
{
public:A(int i = 0):_a(i){cout << "A()" << endl;}void print(){cout << "A:" << _a << endl;}
private:int _a;int _b;
};
int main()
{A a1(10);// 用一个整形变量给A类对象赋值,实际编译器背后会用100构造一个无名对象,最后用无名对象给a2对象进行赋值A a2 = 100;a1.print();a2.print();return 0;
}
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
2.explicit
当我们想要抑制构造函数的类型转换时,我们需要explicit关键字
class A
{
public:explicit A(int i = 0) :_a(i){cout << "A()" << endl;}void print(){cout << "A:" << _a << endl;}
private:int _a;int _b;
};
int main()
{A a1(10);A a2 = 100;a1.print();a2.print();return 0;
}
explicit关键字只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以多个实参的构造函数无需指定为explicit,且只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应该重复。
当我们使用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。
3.委托构造
在C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个委托构造也有一个成员初始值的列表和一个函数体。
在委托构造函数内,成员初始值列表只有唯一的入口,就是类名本身,和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中的另外一个构造函数匹配。
class A
{
public://非委托构造,使用对应的实参进行初始化成员A(int a,int b,int c) :_a(a),_b(b),_c(c){cout << "A(int a,int b,int c)" << endl;}//委托构造A() : A(0,0,0) {cout << "A()" << endl;}//委托构造A(int a) : A(a,0,0) {cout << "A(int a)" << endl;}void print(){cout << "_a:" << _a << " _b" << _b << " _c" << _c << endl;}
private:int _a;int _b;int _c;
};
int main()
{A a1;A a2(10);a1.print();a2.print();return 0;
}
这里默认构造把它委托给三个参数的构造函数,它无需执行任务,我们从结果可以看出,当三个参数的执行过后才执行默认构造,一个参数的构造函数也它委托给三个参数的构造函数。受委托的构造函数会执行,然后控制权才会还给委托者的函数体。
二、类的静态成员
有时间我们的类需要它的一些成员函数与类本身直接相关,而不是与类的各个对象保持关联,这时间我们就可以把这个类的成员声明为静态的。
1.静态成员声明
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
class A
{
public:A();static void pid();//静态成员函数
private:int _a;int _b;static int id;//静态成员变量
};
int A::id = 0;//静态成员变量只能在类外进行初始化。
类的静态成员存在于任何对象之外,对象中不包含任何与静态成员有关数据,从上一篇的类的大小我们也可以看到。因此,只能有一个id对象,且它被所有的A对象共享。
类似的,静态成员函数也不可以与任何对象绑定在一起,他们不包含this指针,作为结果,静态成员函数不可以声明为const的。而且我们也不可以在static函数体内使用this指针。
2.静态成员定义
在类的外部定义静态成员时,不能重复static关键字,该关键字只能出现在类内部的声明语句中。
一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。想要确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
3.静态成员特性
静态成员特性如下:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
三、重载运算符和类型转化
上一章我们已经浅浅的认识了一下重载,这次让我们更加深入的学习重载吧。
1.关系及算数运算符重载
class A
{
public:A(int a = 0,int b = 0):_a(a),_b(b){}A operator+(const A& a1)//重载加号{A tmp;tmp._a = _a + a1._a;tmp._b = _b + a1._b;return tmp;}A& operator+=(const A& a1)//重载加等号{_a += a1._a;_b += a1._b;return *this;}void print(){cout << "_a:" << _a << " _b:" << _b << endl;}
private:int _a;int _b;
};
int main()
{A a1(10, 20);A a2(30, 40);cout << "a1:";a1.print();cout << "a2:";a2.print();A a3 = a1 + a2;a1 += a2;cout << "a1:";a1.print();cout << "a3:";a3.print();
}
问什么调用operator+=来替换operator+更加有效呢?
因为+需要创建一个临时对象,而+=只有一个对象。
class A
{
public:A(int a = 0, int b = 0) :_a(a), _b(b) {}bool operator<(const A& a1)//重载小于号{return (_a < a1._a) && (_b < a1._b);}
private:int _a;int _b;
};
int main()
{A a1(10, 20);A a2(30, 40);if (a1 < a2){cout << "TRUE" << endl;}return 0;
}
这里我们使用的是对象中每一个对象成员均小于另一个对象才返回真。
2.递增递减运算符重载及如何区分
class A
{
public:A(int a = 0, int b = 0) :_a(a), _b(b) {}A& operator++()//重载前置++{_a++;_b++;return *this;}A& operator++(int)//重载后置++{A tmp = *this;_a++;_b++;return tmp;}void print(){cout << "_a:" << _a << " _b:" << _b << endl;}
private:int _a;int _b;
};
int main()
{A a1(10, 20);A a2(30, 40);++a1;//前置++a1.print();A a3 = a2++;a3.print();return 0;
}
我们发现后置++多了一个参数,但我们并没有进行传参,编译器是如何知道我们要调用后置的呢?
后置的版本接受一个额外的(不被使用)int型参数,当我们使用后置运算符时,编译器会提供一个值为0的实参。 尽管从语法上来说我们后置函数会使用这个额外的形参,但在实际过程中通常不会这样做。这个形参的唯一作用就是区分前置版本还是后置版本,而不是真的参加运算。
大家可以自己实现一下前置- -和后置- -;
3.赋值运算符重载
class A
{
public:A(int a = 0, int b = 0) :_a(a), _b(b) {}A& operator=(const A& a1){_a = a1._a;_b = a1._b;return *this;}void print(){cout << "_a:" << _a << " _b:" << _b << endl;}
private:int _a;int _b;
};
int main()
{A a1(10, 20);A a2;a2 = a1;a2.print();return 0;
}
这个我们上一篇已经见到过了。需要注意的是:赋值运算符都必须定义为成员函数。
4.重载输入输出运算符
1.重载输入运算符
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态:而该形参是引用是因为我们无法直接复制一个ostream对象.
class A
{
public:A(int a = 0, int b = 0) :_a(a), _b(b) {}ostream& operator<<(ostream &os){os << "_a:" << _a << " _b:" << _b << endl;return os;}
private:int _a;int _b;
};
int main()
{A a1(10, 20);//cout << a1;a1 << cout;return 0;
}
我们发现打印的方式和我们正常使用的方式不一样,这又是为什么呢?
因为我们调用类的函数的一个参数都是隐含的this指针,所以我们的调用要我们的类对象在前。
为了与标准库兼容,我们的输入输出运算符必须是普通的非成员函数,而不是类的成员函数。
class A
{
public:friend ostream& operator<<(ostream& os, A& a1);A(int a = 0, int b = 0) :_a(a), _b(b) {}
private:int _a;int _b;
};
ostream& operator<<(ostream& os, A& a1)
{os << "_a:" << a1._a << " _b:" << a1._b << endl;return os;
}
int main()
{A a1(10, 20);A a2(20, 40);cout << a1 << a2 << endl;return 0;
}
我们要保证返回的类型也为ostream的引用,这样可以做到连续输出。
2.重载输出运算符
class A
{
public:friend istream& operator>>(istream& is, A& a1);friend ostream& operator<<(ostream& os, A& a1);A(int a = 0, int b = 0) :_a(a), _b(b) {}
private:int _a;int _b;
};
istream& operator>>(istream& is, A& a1)
{int a = 0;int b = 0;is >> a >> b;if (is)//检查输入是否成功{a1._a = a;a1._b = b;}else{a1 = A();//输入失败,对象被赋予默认状态}return is;
}
ostream& operator<<(ostream& os, A& a1)
{os << "_a:" << a1._a << " _b:" << a1._b << endl;return os;
}
int main()
{A a1;cin >> a1;cout << a1 << endl;return 0;
}
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。当我们的读取操作发生错误时,输入运算符应该负责从错误中恢复。
5.函数匹配与重载运算符
重载运算符也是重载函数。因此,通用的函数匹配规则也同样适用于判断在给定的表达式中到底应该使用内置类型运算符还是重载的运算符。
当我们调用一个命名的函数时,具有该名字的成员函数和非成员函数彼此不会重载,这是因为我们用来调用命名函数的语法形式对于成员函数和非成员函数来说是不同的。当我们通过类类型的对象(或者该对象的指针及引用)进行函数调用时,只考虑该类的成员函数。而当我们在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内。
表达式中的运算符的候选函数集既应该包括成员函数,也应该包括非成员函数。我们不可以对一个类既提供了转化目标是算数类型的转化,也提供重载的运算符,这样会产生二义性问题。
四、类的其他细节
1.const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class A
{
public:A(int a = 0, int b = 0) :_a(a), _b(b) {}void print() const{cout << "_a:" << _a << " _b:" << _b << endl;}
private:int _a;int _b;
};
int main()
{A a1;return 0;
}
由于我们不想对该对象进行修改,但我们又不可以在形参上面加入const,所以我们把const加在形参列表后面
编译器会对const进行上述的处理。
2.内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:
内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
内部类的特性
内部类的特性如下:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
public:class B {public:void print(const A& a){cout << "A::_a:" << a._a << " A::_b:" << a._b << " B::id:" << id << endl;}private:int id = 0;};
private:int _a = 0;int _b = 0;
};
int main()
{A::B b;b.print(A());cout <<"A的大小:" << sizeof(A) << endl;return 0;
}
3.匿名对象
class A
{
public:A(int a = 0):_a(a){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
int main()
{A aa1;cout << "到匿名对象了" << endl;A();// 我们可以这么定义匿名对象,匿名对象的特点不用取名字// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数cout << "匿名对象结束了" << endl;A aa2(2);return 0;
}
注意:匿名对象的生命周期只有这一行
4.拷贝时编译器的优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,下面就让我们看看编译器的优化吧。
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a = aa._a;}return *this;}~A(){cout << "~A()" << endl;}
private:int _a;
};
void fun1(A aa){}
A fun2()
{A aa;return aa;
}
int main()
{//传值传参A aa1;fun1(aa1);cout << endl;//传值返回fun2();cout << endl;// 隐式类型,连续构造+拷贝构造->优化为直接构造fun1(1);// 一个表达式中,连续构造+拷贝构造->优化为一个构造fun1(A(2));cout << endl;// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造A aa2 = fun2();cout << endl;// 一个表达式中,连续拷贝构造+赋值重载->无法优化aa1 = fun2();cout << endl;return 0;
}
我们要尽量构造函数和拷贝放进一个句子中,这样可以少量的提升我们的效率。
5.对封装的进一步认识
类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
总结
到这里我们的类已经入门了。想要对类进一步了解我们还要学习继承和多态,进一步的学习需要我们熟练的使用我们现在所学的类的内容,所以下一步我们会进入容器的学习。