一.再探构造函数之初始化列表
• 之前实现构造函数时,初始化成员变量都是在函数体内赋值,构造函数初始化还有一种方式:初始化列表。初始化列表的使用方式是:在函数定义处的参数列表后(在声明和定义分离时就在真正定义的地方写),以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面都跟一个放在括号中的初始值或表达式:
cpp
class Date
{
public:
Date(int& x, int year, int month, int day)
:_year(year), _month(month), _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
• 每一个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量①定义和②初始化的地方,而我们写的函数的函数体内是定义、初始化完毕后进行赋值的地方。
• ①引用成员变量,②const成员变量,③没有默认构造的类类型变量,这三种必须放在初始化列表位置(其定义的地方)进行初始化,否则会编译报错。
cpp
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "调用Time()函数" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year, int month, int day)
:_year(year), _month(month), _day(day), _t(12), _rx(x), _n(1)
{
}
private:
int _year;
int _month;
int _day;
Time _t; //没有默认构造的类类型变量
int& _rx; //引用成员变量
const int _n; //const成员变量
};
其本质原因在于:
①引用成员变量,语法要求引用变量必须在定义后立即给一个对象完成初始化,且后续不能改变其引用对象。
②const成员函数,其在于后续的不可更改性,无法在函数体内完成赋值操作,于是乎必须在初始化的时候就把想要给的值给他。
③没有默认构造的类类型变量,在进入构造函数体之前,编译器会尝试自动调用其默认构造函数。因其没有默认构造,编译器就会报错。必须通过初始化列表显式传参,调用其指定的带参构造函数。
• C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的,意思是如果你没有显式的在初始化列表里给这个变量值的化,编译器会用这个变量的缺省值在初始化列表初始化。
cpp
class Time
{
friend class Date;
public:
Time(int hour)
:_hour(hour)
{
cout << "调用Time()函数" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date()
:_month(2)
{
cout << "Date()" << endl;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << _t._hour <<' ' << _n <<' ' << _ptr << endl;
}
private:
// 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的
// 如果初始化列表没有显示初始化,默认就会用这个缺省值初始化
int _year = 1;
int _month = 1;
int _day;
Time _t = 1;//内置类型也能给缺省值,若有多个参数就写成:Time t1{10,4};
const int _n = 1;
int* _ptr = (int*)malloc(12);
};
int main()
{
Date d1;
d1.Print();
return 0;
}
在这里我们可以试验出,_month是根据在初始化列表里显式传的值初始化的,而其他变量是在初始化列表用缺省值初始化的。
• 初始化列表会按照成员变量在类中的声明顺序进行初始化,跟成员在初始化列表中出现的先后顺序无关。为了避免一些奇奇怪怪的错误,建议初始化列表顺序和声明顺序保持一致。
• 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表去初始化,①如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。②如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。③对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。但是函数体的用处也是不可替代的,毕竟一些语句如判断,判空等也只能写在函数体。
综上,对初始化列表的语法总结:
无论是否显示写初始化列表,每个构造函数都有初始化列表;
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
对初始化列表的使用总结:推荐使用初始化列表初始化成员变量
1.可以显式在初始化列表初始化
2.建议声明时给缺省值,若未显式初始化,初始化列表也会用这个值初始化该成员

二.类型转换
• C++支持内置类型隐式转换成类类型对象,但需要有以该内置类型为参数的构造函数。
cpp
#include<iostream>
using namespace std;
class A
{
public:
//①号单参数构造
A(int a1)
:_a1(a1)
{ }
//②号多参数构造
A(int a1, int a2)
:_a1(a1), _a2(a2)
{ }
void Printf()
{
cout << _a1 << ' ' << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//隐式类型转换,2先用①号构造转换成A类型的对象,然后再拷贝构造aa1对象
A aa1 = 1;
aa1 = 2;
aa1.Printf();
//在这里aa2引用了3所转化的临时变量,正常的临时变量其声明周期只有一行,但是因为我对其引用,所以其生命周期就跟着aa2走了
const A& aa2 = 3;
//C++11之后支持了多参数转换,通过②号构造函数转换来的
A aa3 = { 3,4 };
aa3 = { 1,6 };
aa3.Printf();
return 0;
}
• 类类型的对象之间也可以隐式转换,和内置类型转类类型一样,需要有对应参数的构造函数支撑
cpp
class A
{
public:
A(int a1, int a2)
:_a1(a1), _a2(a2)
{
}
void Printf()
{
cout << _a1 << ' ' << _a2 << endl;
}
int add()const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.add())
{
}
void Printf()
{
cout << _b << endl;
}
private:
int _b = 0;
};
int main()
{
A aa1 = { 2,1 };
//隐式类型转换
B b = aa1;
aa1.Printf();
b.Printf();
return 0;
}
• 在构造函数的前面加上explicit就不再支持隐式类型转换
cpp
class A
{
public:
explicit A(int a)
:_a(a)
{ }
private:
int _a = 0;
};
int main()
{ //编译报错:不存在从"int"转换到"A"的适当构造函数
//A aa1 = 2;
A aa1(2);//而正常构造没问题
return 0;
}
三.static成员(变量和函数)
• 用static修饰的成员变量,称之为静态成员变量,静态成员变量为所有类对象所共享,不属于某个具体的对象,其也不存在于对象中,而是存放在静态区。
• 静态成员变量一定要在类外初始化
• 可以这样理解,就是把一个全局变量放入了类中,受到类域限制和管理,但不存在某个具体对象中,而是整个类的"全局",所有的对象共用同一个static成员变量。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是去转到构造函数初始化列表的,而静态成员变量不属于某个对象,不走构造函数初始化列表。
cpp
class A
{
public:
A()
{
++_count;
}
A(const A& t)
{
++_count;
}
~A()
{
--_count;
}
int Get()
{
return _count;
}
private:
//类中的声明
static int _count;
};
//类外正式定义并初始化,编译器为其分配内存
int A::_count = 0;
int main()
{
A a1, a2;
A a3(a1);
//在这里并没有显式的对_count去操作,而是通过函数调用一并更改,也能同时证明_count并不存在于某一对象中,所有类对象共用一个_count
cout << a1.Get() << endl;
cout << a2.Get() << endl;
return 0;
}
• 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针,这也就导致其不可访问非静态成员,不知道去访问哪个对象了。
• 静态成员函数中可以访问静态成员,但是不能访问非静态的,因为没有this指针。
• 非静态成员函数可以访问任意的静态成员变量和静态成员函数。
• 突破类域就可以访问静态成员,可以直接通过 类名::静态成员 或者 对象.静态成员来访问静态成员变量和静态成员函数。
cpp
cout << a1.Get() << endl;
cout << A::Get() << endl;
• 静态成员也是类的成员,受public、protected、private访问限定符的限制
cpp
class A
{
public:
A()
{
++_count;
}
A(const A& t)
{
++_count;
}
~A()
{
--_count;
}
static int Get()
{
return _count;
}
private:
//类中的声明
static int _count;
};
//类外正式定义并初始化,编译器为其分配内存
int A::_count = 0;
int main()
{
cout << A::Get() << endl;
A a1, a2;
A a3(a1);
cout << a1.Get() << endl;
//支持用类名:: 去访问了
cout << A::Get() << endl;
//编译报错:error C2248: "A::_count": 无法访问 private 成员(在"A"类中声明)
//cout << A::_count << endl;
return 0;
}
四.友元
• 友元提供了一种突破类访问限定符封装的方法,友元分为:友元函数和友元类。在函数或类的声明前面放一个friend,并把这个友元声明放入一个类的里面。
cpp
class A
{ //友元函数和类的声明
friend int add(int x, int y);
friend class B;
public:
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
private:
int _b = 0;
};
int add(int x, int y)
{
return x + y;
}
• 外部友元函数/类可以访问类的私有和保护成员,友元函数/类仅仅是一种声明,他不是类的成员函数/类
cpp
class A
{
//友元函数和类的声明
friend int add(const A& a);
public:
A(int a1,int a2)
:_a1(a1),_a2(a2)
{ }
private:
int _a1 = 1;
int _a2 = 2;
};
int add(const A& a)
{
//add为A的友元函数,可以之间访问A的私有
return a._a1 + a._a2;
}
int main()
{
A aa1(4, 5);
int sum = add(aa1);
cout << sum << endl;
return 0;
}
• 友元声明可以写在类的任何地方,其不受访问限定符限制
cpp
class A
{
public:
private:
//友元函数和类的声明
friend int add(int x, int y);
friend class B;
int _a1 = 1;
int _a2 = 2;
};
class B
{
private:
int _b = 0;
};
int add(int x, int y)
{
return x + y;
}
• 一个类或函数可以是多个类的友元类或友元函数
• 友元类中的成员函数都是另一个类的友元函数,都可以访问另一个类中的私有和保护
cpp
class A
{
//友元类的声明
friend class B;
public:
A(int a1,int a2)
:_a1(a1),_a2(a2)
{ }
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
int add(const A& a)
{
//add为A的友元函数,可以之间访问A的私有
return a._a1 + a._a2;
}
};
这里B是A的友元类,所以B中的add是A的友元函数。
• 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
• 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,B类是C类的友元,但是A类不是c类的友元。
• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五.内部类(了解)
• 如果一个类定义在另一个类的内部,这个在内部的类就叫做内部类。
• 内部类是一个独立的类(和定义在全局的类一样),并不属于包含它的类(算空间时算不上它),跟定义在全局相比,它只是受外部类的类域限制和访问限定符限制。(这里和static成员有点像)
• 内部类默认是外部类的友元类,即内部可以访问外部的私有和保护,而默认外不可以访问内的。
• 内部类本质也是一种封装,当A类和B类紧密相连时,A类实现出主要就是给B类用的,则可以把A类设计成B的内部类,再在B中写A类的友元声明,如果放到private/protected位置,那A就是B的专属内部类,其他地方都不可以使用了。
cpp
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
六.匿名对象
• 用 "类型(实参)" 定义出来的对象叫匿名对象,相比我们之前定义的"类型 对象名(实参)"定义出来的叫有名对象。
cpp
int main()
{
//有名对象
A aa1(1, 2);
//匿名对象
A(2, 3);
return 0;
}
• 匿名对象生命周期只在当前一行,其用处在于,①一般定义一个对象当前用一下②我定义一个对象只为了调用其类内部的某个函数,就可以定义匿名对象。
cpp
int main()
{
//匿名对象
A();
A(1);
//创建对象只为了调用函数
Solution().Sum_Solution(10);
return 0;
}