文章目录
- 构造函数再探
- 以下代码共调用多少次拷贝构造函数
- 委托构造函数
- 概念
- 形式
- 匿名对象
- 友元
- 友元的声明
- 友元类
- 令成员函数作为友元
- 函数重载和友元
- 注意
- 内部类
- 特性
- 类的const成员
- 可变数据成员
- 类的static成员
- 概念
- 关于static
- 静态成员的类内初始化
- 静态成员能用于某些普通成员不能的场景
构造函数再探
以下代码共调用多少次拷贝构造函数
Widget f(Widget u)
{ Widget v(u);Widget w=v;return w;
}int main(){Widget x;Widget y=f(f(x));}
这道题我第一眼看,在调用f的时候,会用实参来构造对象u,在用对象u构建v,然后用v构建w,最后因为是传值返回,会用w再构造一个临时量,所以调用一次f会使用4次拷贝构造函数,最后再用这两次f的返回值来构造y,应该是9次。
但是答案却是7次,让我十分不解,所以我去论坛搜索了一下,发现这里涉及到了编译器的优化。
我们可以看到,在第一次调用f的结束的时候,会返回一个由w构造的临时量,再将这个临时量作为实参来初始化第二个f的形参u,(第二个f调用到return这一步时,返回一个由w构造的临时量,再将这个临时量作为实参来初始化拷贝构造函数的形参)编译器觉得这一步有点多余,会将其优化为一步(第一次直接用w构造u,第二次直接用w构造y),所以每次调用f的时候其实只经过了3次的拷贝构造,最后再加上y的拷贝构造,一共是7次。
委托构造函数
概念
使用它所属类的其他构造函数执行它自己的初始化过程,换言之,它把它自己的一些(或者全部)指责委托给了其他构造函数。
形式
有一个成员初始值的列表和一个函数体。成员初始值列表只有一个唯一的入口,即类名本身。
class Date
{
public:// 非委托构造函数Date(int year, int month, int day):_year(year), _month(month), _day(day) { }// 其余构造函数全部委托给上面的构造函数Date():Date(0,1,1){}Date(int i):Date(i,0,0){}Date(std::istream &is):Date(){is >> this->_year >> this->_month >> this->_day;}void pr(){cout << this->_year << ends << this->_month << ends << this->_day << endl;}
private:int _year;int _month;int _day;
};
本例中,共有四个构造函数。
第一个构造函数接受三个实参,使用这些实参初始化数据成员,然后结束工作。
第二个默认构造函数使用三参数的构造函数完成初始化过程,因为函数体为空可知无需再执行其他任务。
第三个构造函数接受一个实参,同样委托给了三参数的构造函数。
第四个构造函数先委托给了默认构造函数,默认构造函数又接着委托给了三参数构造函数。当这些受委托的构造函数执行完后,接着执行std::istream &is构造函数体的内容。
ps:当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体依次被执行,然后控制权才会交还给委托者的函数体。
匿名对象
当我们想要调用对象中的一个方法,或者只是想在这一句使用这个对象,其他地方不再使用该对象的时候。如果我们直接构造一个对象使用,这无疑是一种很大的浪费,因为这个对象我们用了一次就扔了,不再需要了,而一个对象的生命周期是整个栈帧。
这时就需要用到匿名对象,匿名对象是一种临时对象,它的生命周期只有使用它的那一行语句,执行完则立即销毁
class Date
{int _year;int _month;int _day;
public:Date(int year = 2020, int month = 4, int day = 24){_year = year;_month = month;_day = day;cout << "gouzao" << this << endl;}void print(){cout << this->_year << endl;}~Date(){cout << "xigou" << this << endl;}
};int main()
{Date d1; //创建一个对象,生命周期为整个函数栈帧d1.print();Date().print();//创建一个匿名对象,生命周期只有这一行语句,实行完则立即调用析构函数Date d2; //创建一个对象,生命周期为整个函数栈帧d2.print();return 0;
}
运行结果:
友元
类中的private部分非类的成员是无法访问的。但类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
友元的声明
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。一般来说,最好在类定义开始或结束前的位置集中声明友元。
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。
ps:上述代码想讲的还是要知道友元声明的作用是规定访问权限,不能起到普通意义声明的作用。
友元类
class A
{friend class Date;//将Date声明为友元类,Date可以访问A的所有成员变量
private:string str;
};
class Date
{
public:void Print(){cout << a.str << endl;//访问a的私有成员}
private:int _year;int _month;int _day;A a;
};
令成员函数作为友元
如果不想让整个类作为自己的友元,也可以只为那个需要访问自己的private对象的函数提供访问权限。当把一个成员函数声明成友元时,必须明确指出该成员函数属于哪个类:
class A;
class Date
{
public:void Print(A);private:int _year;int _month;int _day;
};class A
{friend void Date::Print(A);//将Date的成员函数Print声明为友元,Print可以访问A的所有成员变量
private:string str;
};void Date::Print(A a){cout << a.str << endl;//访问a的私有成员
}
要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。该例中,我们必须按照如下方式设计程序:
- 首先前向声明A类(因为Print的声明会用到)
- 定义Date类,声明Print函数,但不能定义它(因为要访问A的私有成员,A还没有被定义好,无法访问其私有成员)
- 定义A类,包括对Print的友元声明
- 定义Print,此时它才可以使用A的成员
函数重载和友元
尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,需要对这组函数中的每一个分别声明:
在A中并没有关于Print(A, A)的友元声明,因此其不能访问A的私有成员。
注意
1.友元关系是单向的,不具有交换性。
Date为A的友元,可以访问A的私有成员,但是A并不能访问Date的
2.友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
内部类
有没有想过,既然类的成员变量可以是自定义类型,那能不能在类中再构建一个类呢?
class Date
{
public:void Print(){cout << _year << endl;}private:class A{public:void Print(){cout << _data << endl;}private:int _data;};int _year;int _month;int _day;};
可以看到,这是可以的,但是这两个类有什么关系吗?
这个内部类其实是一个独立的类,它不属于外部类,同时外部类对它也没有任何特权,但是它同时还是外部类的友元类,可以通过外部类的对象参数来访问外部类的所有成员。
特性
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系
类的const成员
因为对于类和对象,封装性是一个很重要的东西,但是访问限定符只对外部有影响,对自身的成员函数没有影响,如果我们不想让一个成员函数对类的成员进行修改,这时,就需要用const来修饰成员函数。
class Date
{
public://等价于 void print(const Date* this)void Print() const{cout << _year << '-' << _month << '-' << _day << endl;}
private:int _year;int _month;int _day;};
对于用const修饰的成员函数,需要将const放在最后面,来区分开const参数和const返回值。这里的const其实修饰的是该成员函数的this指针,所以该成员函数就无法对类的成员进行修改。
-
const对象可以调用非const成员函数吗?
答案:不行,因为const对象的只能读不能写,而非const的成员函数则可读可写,使权限放大了,不行。 -
非const对象可以调用const成员函数吗?
答案:可以,因为非const对象的可读可写,const成员函数只可读,使权限缩小,可行。
-
const成员函数内可以调用其它的非const成员函数吗?
答案:不行,因为const成员函数的this指针是常量,只可读,而非const的成员函数则可读可写,使权限放大了,不行。 -
非const成员函数内可以调用其它的const成员函数吗?
答案:可以,因为非const成员函数的this指针可读可写,const成员函数只可读,使权限缩小,可行。
可变数据成员
有时会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字实现这一目的。
一个可变数据成员永远不会是const,即使它是const对象的成员。因此,一个const成员函数可以改变成员的值。
举个例子,我们需要一个可变成员num来追踪每个成员函数被调用了多少次:
class Date
{
public:void Print() const{num++;cout << num << endl;}private:mutable size_t num = 0;
};int main(int argc, char const *argv[]) {Date d;d.Print();d.Print();d.Print();return 0;
}
运行结果:
尽管Print是一个const成员函数,它仍然能够改变num的值。因为num是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值。
类的static成员
概念
有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。这样的成员叫做类的静态成员。
关于static
我们通过在成员的声明前加上关键字static使得其与类关联在一起。对于用static修饰的成员函数,称为静态成员函数,成员变量称为静态成员变量。因为静态的成员的生命域不在类中,在静态区,所以静态的成员只能在类外初始化。
- 静态成员为所有类对象所共享,不属于某个具体的实例
- 静态成员变量必须在类外定义,定义时不添加static关键字,一个静态数据成员只能定义一次
- 静态成员函数可以在类内部也可以在类外部定义,在类外部定义时,不能添加siatic关键字(外部有static关键字表示这个函数在文件内有效)
- 类静态成员亦可用 类名::静态成员 或者 对象.静态成员 或者 指向对象的指针->静态成员 来访问
- 静态成员函数不与任何对象绑定在一起,它们不包含this指针(static成员函数也就不能被声明成const,上章提到const是为了将this指针赋予底层const),也不能在static函数体内使用this指针,不包含this指针也就不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public、protected、private3种访问类别,也可以具有返回值
- 静态成员和全局变量虽然都存储在静态区,但是静态成员的生命周期只在本文件中,而全局变量不是
因为静态成员函数不属于某个对象,所以它没有this指针,无法访问任何非静态的成员,但是非静态的成员函数具有this指针,可以不用通过作用域运算符直接使用静态成员。
关于第二点和第三点的代码示例:
class Date
{int db = 666;static int num;static double init();
public:static int print();void test(){db += db * num;// 成员函数不用通过作用域运算符就能直接使用静态成员}
};
int Date::num = init(); // 定义并初始化一个静态成员int main(int argc, char const *argv[]) {int r;r = Date::print(); // 使用作用域运算符访问静态成员Date d1;Date *d2 = &d1;r = d1.print(); // 通过Date的对象或引用r = d2->print(); // 通过指向Date对象的指针return 0;
}
关于第二点:
从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了。因此,我们可以直接使用init函数,虽然其是私有的。
静态成员的类内初始化
通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。
class Date
{constexpr static int num = 2*3; // num是常量表达式double db[num];
public:
};
如果某个静态成员的应用场景仅限于编译器可以替换他的值的情况,则一个初始化的const或者static constexpr不需要分别定义。相反,如果我们将它用于值不能替换的场景中,该成员必须有一条定义语句。
如果在类的内部提供了一个初始值,则在类外部成员的定义不能再指定一个初始值了:
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
静态成员能用于某些普通成员不能的场景
- 静态数据成员可以是不完全类型(声明之后定义之前)
- 静态数据成员的类型可以就是他所属的类类型。而非静态数据成员只能声明成他所属类的指针或引用
- 可以使用静态成员作为默认实参
非静态数据成员不可以,因为它的值本身属于对象的一部分这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。
(图中error:非静态数据成员“Date::test”的使用无效)