这篇博文是C++中类和对象的最后一些知识,包括再探构造函数、类型转换、static成员、友元、内部类、匿名对象、拷贝对象时编译器的优化这些知识点。
1.再探构造函数
之前我们实现构造函数时,初始化成员变量主要是使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表。
初始化列表的使用方式:冒号开始,逗号分隔数据成员列表,每个成员变量后面跟一个放在括号里的初始值或者表达式。
class Date
{
public:
Date(int year, int month, int day)
:_year(year) //冒号开始,逗号分隔
,_month(month) //成员函数后括号里放初始值
,_day(day + 1) //括号里放也可以是表达式
{} //大括号不能丢
private:
int _year;
int _month;
int _day;
};
初始化列表大概就是上面这样。
每个成员变量在初始化列表只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
我们有成员变量的声明,对象的定义,那成员变量在哪里定义?就在初始化列表。
const成员变量,引用成员变量,没有默认构造的类类型变量,必须在初始化列表位置进行初始化,否则会编译报错。
先说const成员变量和引用成员变量为什么必须在初始化列表进行初始化。
普通变量定义之后可以初始化也可以不初始化,编译器建议我们初始化。
int a; //不初始化
int b = 1; //初始化
但是const变量必须初始化,而且只有一次初始化的机会,就是在const变量定义的时候。
//不能像下面这样
const int x;
x = 1;
const int x = 1; //只能这样
所以const成员必须在初始化列表初始化,因为这是它定义的地方。
引用成员变量也是一样,在定义的地方就要初始化,所以要在初始化列表初始化,不能在函数体里面,因为前面说过,初始化列表才是成员函数定义的地方。
class Date
{
public:
Date(int num, int year, int month, int day)
:_year(year)
,_month(month)
,_day(day + 1)
,_n(1)
,_ref(num) //ref此时就是x的引用
{}
private:
int _year; //可在函数体内初始化
int _month;
int _day;
const int _n; //必须在初始化列表初始化
int& _ref;
};
int main()
{
int x = 1;
Date d1(x, 2024, 1, 1);
return 0;
}
再说一下没有默认构造的类类型变量在初始化列表初始化的情况。
class Time
{
public:
Time(int hour = 0) //Time的默认构造函数
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int num, int year, int month, int day)
:_year(year)
,_month(month)
,_day(day + 1)
,_n(1)
,_ref(num) //但是没有在初始化列表初始_t
{}
private:
int _year;
int _month;
int _day;
const int _n;
int& _ref;
Time _t; //声明一个Time的成员变量_t
};
没有在初始化列表初始化_t,运行的时候就会走Time的默认构造函数进行初始化,如果没有默认构造函数呢?运行后发现直接报错了。
在没有默认构造函数的情况下,初始化_t必须走初始化列表。
Date(int num, int year, int month, int day)
:_year(year)
,_month(month)
,_day(day + 1)
,_n(1)
,_ref(num)
,_t(1) //_t走初始化列表
{}
所以当类类型变量有默认构造函数时,在初始化列表可写可不写,没有默认构造函数时,必须在初始化列表进行初始化。
C++11允许在成员函数声明的地方给缺省值,这个缺省值是给初始化列表用的。这个缺省值也不止可以是值,还可以是表达式。
变量定义了也可以不初始化,比如下面的_day
class Date
{
public:
Date(int num, int year, int month, int day)
:_year(year)
,_month(month)
{}
private:
int _year;
int _month;
int _day;
};
那此时_day是随机值
//这里依然是成员变量的声明,后面的值是缺省值
private:
int _year = 1;
int _month = 1;
int _day = 1;
在声明的地方加了缺省值后我们再来看_day的值。
所以我们在初始化列表给了初始值,就用给的值,如果没给,就用声明的地方给的缺省值,如果也没有缺省值,就是随机值,这取决于编译器。
在声明的地方给缺省表达式情况举下面这个例子。
private:
int _year = 1;
int _month = 1;
int _day = 1;
int* _ptr = (int*)malloc(12); //表达式
尽量使用初始化列表初始化,因为那些不在初始化列表初始化的成员也会走初始化列表。
初始化列表中按照成员变量在类中声明顺序进行初始化,先声明先初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
2.类型转换
先看A这个类
class A
{
public:
A(int a)
:_a1(a)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
一般我们调用成员函数Print像下面a1这样,那a2是什么意思?
int main()
{
A a1(1);
a1.Print();
A a2 = 2;
a2.Print();
return 0;
}
a2其实就是一种隐式类型转换。我们之前说过类型转换会产生一个临时对象。
但是编译器遇到连续构造+拷贝构造时,会优化为直接构造。
再拿栈的Push举例。
//原本代码
Stack st;
A a3(3);
st.Push();
//类型转换代码
st.Push(3);
但是,单参数的才能这么写,多参数其实也支持,就是有一点点不一样。
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
A a3 = 1, 1; //错误示范
A a3 = { 1, 1 }; //正确写法
也就意味着支持下面这种写法
const A& ra4 = { 2, 2 };
用栈举例的话,如下两种写法都可以。
Stack st;
st.Push(a3);
st.Push({2, 2});
这里都是类型转换。在C++11之后才支持。如果不想这种转换发生,就在成员函数前面加一个explicit。
explicit A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
有了类型转换,写起来更简单。
3.static成员
用static修饰的成员变量称为静态成员变量,静态成员变量一定要在类外进行初始化。
静态成员对象为当前类的所有对象所共享,不属于某个具体的对象,不存在对象中,放在静态区。
class A
{
public:
//...
private:
//类里面声明
static int _scount;
};
int A::_scount = 0; //类外面初始化
static成员也不能在声明的地方给缺省值,缺省值是给初始化列表用的,这里不走初始化列表。
用static修饰的成员函数称为静态成员函数,静态成员函数没有this指针。
静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
比如说我们想访问一下_scount,就不能直接访问。
private:
//类里面声明
static int _scount;
需要写一个静态成员函数去访问。
class A
{
public:
static int GetACount() //静态成员函数
{
return _scount;
}
private:
static int _scount; //静态成员变量
};
因为没有this指针,所以调用静态成员函数的时候就直接指定类域调用。
int main()
{
//指定类域调用,打印出来
cout << A::GetACount() << endl;
return 0;
}
我们在写一个构造函数,拷贝构造函数,析构函数,并且调用构造函数和拷贝构造函数时对_scount加加,调用析构函数时对_scount减减。
class A
{
public:
A() //构造
{
++_scount;
}
A(const A& t) //拷贝构造
{
++_scount;
}
~A() //析构
{
--_scount;
}
static int GetACount() //静态成员函数
{
return _scount;
}
private:
static int _scount; //静态成员变量
};
再实例化3个A的对象出来
int A::_scount = 0; //类外面初始化
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
这里体现不出来析构的减减,我们可以写一个代码块的局部域
如果说static成员函数想访问普通成员变量也是不可以的。
class A
{
public:
//...
static int GetACount() //静态成员函数
{
_a++; //错误示范,不可访问,因为没有this指针
return _scount;
}
private:
static int _scount; //静态成员变量
int _a; //普通成员变量
};
非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
class A
{
public:
//...
static int GetACount() //静态成员函数
{
return _scount;
}
void func() //非静态成员函数
{
_scount++;//访问静态成员变量
cout << GetACount() << endl;//访问静态成员函数
}
private:
static int _scount; //静态成员变量
};
4.友元
友元提供了一种突破类访问限定符封装的方式,友元分为友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,并不是类的成员函数。
友元函数的声明可以在类定义的任何地方声明,不受访问限定符的限制。
class Date
{
private:
int _a = 1; //私有成员变量
};
void func(Date& rd)
{
cout << rd._a << endl; //不能访问私有成员
}
int main()
{
Date d;
func(d);
return 0;
}
class Date
{
friend void func(Date& rd); //加上友元声明,就能访问类的私有了
private:
int _a = 1; //私有成员变量
};
void func(Date& rd)
{
cout << rd._a << endl;
}
int main()
{
Date d;
func(d);
return 0;
}
一个函数可以是多个类的友元。
class B; //一定要加这个前置声明,不然A类里面找不到B类会报错
class A
{
friend void func(A& rd, B& rb); //友元声明
private:
int _a = 1; //私有成员变量
};
class B
{
friend void func(A& rd, B& rb); //友元声明
private:
int _b = 2;
};
void func(A& rd, B& rb) //在A和B类中都有友元声明
{
cout << rd._a << endl;
cout << rb._b << endl;
}
int main()
{
A d;
B b;
func(d, b);
return 0;
}
友元类的成员函数都可以是另一个类的友元函数,都可以访问另一个类的私有和保护成员
class B; //一定要加这句代码,不然A类里面找不到B类会报错
class A
{
friend class B; //友元声明
private:
int _a = 1; //私有成员变量
int _c = 3;
};
class B
{
public:
void func(A& rd, B& rb) //成员函数
{
cout << rd._a << endl; //访问A的私有
cout << rb._b << endl;
cout << rd._c << endl; //访问A的私有
}
private:
int _b = 2;
};
一般不要对成员函数进行友元声明,这样不太好。
友元类是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
友元类关系不能传递,如果A类是B类的友元,B类是C类的友元,A类是C类的友元吗?不是
友元虽然提供了便利,但是会增加耦合度,破坏封装,所以不宜多用。
5.内部类
如果一个类定义在另一个类的内部,这个定义在内部的类就是内部类。
内部类是一个独立的类,跟定义在全局相比,内部类只是受外部类的类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
内部类默认是外部类的友元。
class A
{
private:
static int _k;
int _h = 1;
public:
class B //内部类
{
public:
void foo(const A& a)
{
cout << _k << endl;
}
private:
int _b = 2;
};
};
int A::_k = 3;
为了证明内部类是独立的,我们看一下A类的大小是多少,对A类sizeof看大小。
结果是4个字节。我们分析一下,假设B不独立,B就在A里面。
内部类只是受类域限制和访问限定符限制。我们要实例化B的对象的话要像下面这样写。
A::B b; //突破类域限制,实例化B的对象
但是如果B类在A类里是私有的,受访问限定符的影响,B也是不能被访问的。
class A
{
private:
static int _k;
int _h = 1;
//public:
class B //内部类成了私有,不可被任意访问
{
public:
void foo(const A& a)
{
cout << _k << endl;
cout << a._h << endl;
}
private:
int _b = 2;
};
};
int A::_k = 3;
内部类本质也是一种封装,当A类和B类紧密关联,A类实现出来主要就是给B类使用的,那么就可以考虑把A类设计为B的内部类,如果放到private/protect的位置,那么A类就是B类的专属内部类,其他地方用不了。
6.匿名对象
之前说过,实例化对象的时候如果没有参数要传的话,只能像下面的1那样写,不能像2那样写
A aa1; //1可以
A aa2(); //2不可以
因为像2这样定义的话,编译器无法识别这是一个函数声明还是对象定义。但是可以像下面这样。
A();
这就是匿名对象。前面的aa1,aa2也叫做有名对象。
匿名对象也可以传参。
A(1); //一个参
A(1, 2, 3); //多个参
匿名对象调用举例如下:这里有一个A类
class A
{
public:
void Print()
{
cout << _i << endl;
}
private:
int _i = 1;
};
int main()
{
//有名对象调用,
A a;
a.Print();
//匿名对象调用
A().Print();
return 0;
}
匿名对象就是为了更方便一点。但是生命周期短,只在当前这一行。 有名对象的生命周期当前这个作用域。
7.对象拷贝时的编译器优化
• 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传参过程中可以省略的拷贝。
• 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更“激进"的编译还会进行跨行跨表达式的合并优化。
这次分享就到这里,拜拜~