
这里是think的博客
希望可以一起交流知识,一起think
今天我们来学习C++类和对象(四)
一起来think
1. 再探构造函数
1.1 初始化列表
1.1.1 初始化列表形式
class A
{
public:
A(int a = 0)
:_a1(a),_a2(2)
,_a3(4)
{
cout << "A(int a)" << endl;
}
private:
int _a1 = 1;
int _a2;
int _a3;
};
这里的 :_a1(a),_a2(2),_a3(4)就是初始化列表,可以看到初始化列表是以冒号开始,对成员变量进行初始化,其中以逗号分隔各个成员变量,其中可以换行,可以不换行,建议换行,防止一行太长。
初始化列表中可以用值,初始化成员变量,也可以用表达式初始化
:p = ((int*)malloc(sizeof(int)*6));
1.1.2 初始化列表的本质
初始化列表可以认为是成员变量定义初始化的地方,即
private:
int _a1 = 1;
int _a2;
int _a3;
这里是类成员变量中的声明,语法上认为初始化列表是定义时初始化的地方,所以既然是定义时初始化的地方,那么每一个成员变量都要经过初始化列表。
1.1.3 初始化列表的先后顺序
初始化列表是定义时初始化的地方,但是初始化的顺序并不是按照初始化列表来的,而是按照声明顺序来的。
1.1.4 初始化列表的缺省值
private:
int _a1 = 1;
int _a2;
int _a3;
这个int _a1 = 1;就是缺省值,即如果初始化列表中没有这个成员变量,初始化列表就会用这个来初始化。
1.1.5 两个缺省值对比
class A
{
public:
A(int a = 0)
:_a1(a),_a2(2)
,_a3(4)
{
cout << "A(int a)" << endl;
}
private:
int _a1 = 1;
int _a2;
int _a3;
};
int main()
{
A a(1);
return 0;
}
可以看到初始化列表的缺省值和函数形参的缺省值是比较容易混淆的,函数形参的缺省值是在该处没有参数时使用缺省值传递给形参,而初始化列表的缺省值则是在没有显示写的时候,用缺省的初始化成员变量。
1.1.6 一定要在初始化列表中显式写的变量
对于const 成员变量,引用成员变量,没有默认构造的类类型变量一定要在初始化列表中显式写,对于前面两个我们都知道是只能在定义时初始化,之后就不能修改对应的值了。
对于没有默认构造的类类型变量,除了在没有默认构造函数的类中补上默认构造函数外,解决方法就是在初始化列表中显式初始化了。
1.1.7 初始化列表的总结
初始化列表分为显式写和不显式写的
- 显式写的就用显式写的来初始化成员变量
- 不显式写的如果有声明处缺省值(a)就用缺省值,没有缺省值(b)如果是内置类型(x)一般编译器不做处理,但是也可能初始化为0,如果是自定义类型(y)调用它的默认构造函数,如果没有默认构造函数就报错。

2 类型转换
#include<iostream>
using namespace std;
class A
{
public:
A(int a1)
:_a1(a1)
{
}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{
}
private:
int _b = 0;
};
int main()
{
A aa1 = 1;
aa1.Print();
const A& aa2 = 1;
A aa3 = { 2,2 };
B b = aa3;
const B& rb = aa3;
return 0;
}
2.1 单参数隐式类型转换
A aa1 = 1;语法就是1隐式类型转换为A类型的临时对象(这个过程又叫做构造),再拷贝构造给aa1,因为构造再拷贝构造太繁琐了,可以优化,所以一般现在的编译都会优化为直接构造,即1直接构造aa1。
其中 const A& aa2 = 1;这里就不会优化了,因为没有连续的构造,这里就只是1隐式类型转换为A的临时对象(构造),然后aa2引用这个临时对象。
2.2 多参数隐式类型转换
在C++11中才有多参数隐式类型转换,其中单参数是没有括号的,而多参数的则是由一个大括号,优化也是和单参数一样的。
2.3 类类型之间的隐式类型转换
class B
{
public:
B(const A& a)
:_b(a.Get())
{
}
private:
int _b = 0;
};
B b = aa3;
const B& rb = aa3;
return 0;
这里其实说明了,不只是整型才可以隐式类型转换,如果参数中有其他类类型的对象,类类型之间也是可以隐式类型转换的,至于优化和整型是一样的。
2.4 expilcit — 禁用隐式类型转换
explicit A(int a1, int a2),加上这个关键字后,就不能在隐式类型转换了如A aa3 = { 2,2 };必须要显式写成这样A aa3(2,2);
3 static成员
3.1 static成员变量
3.1.1 static成员变量初始化的位置
static成员要在类外初始化,同时初始化的时候不需要加上static,声明时加即可,因为static即表示该变量存入静态区,后续初始化就没有必要再加了。
#include<iostream>
using namespace std;
class A
{
public:
A(int a1)
:_a1(a1)
{
}
static int Get()
{
return _a2;
}
private:
int _a1 = 1;
static int _a2;
};
int A::_a2 = 100;
int main()
{
cout << A::Get() << endl;
return 0;
}
3.1.2 static成员变量的从属
static成员变量不同于其他的成员变量,static成员变量不属于某一个对象,而是被类的所有对象所共享,并且static成员变量也不在对象中,而是在静态区中。(可以认为static成员变量是公共的,而其他成员变量是私有的)
3.1.3 static成员变量的访问
static成员变量是公共的,那么可以A::_a2;也可以A a; a._a2;(pubilc下才可以这样访问,否则要写一个函数来访问,这也说明static成员变量受访问限定符的限制,当然static成员函数也受限制)
3.1.4 static成员变量不能给缺省值
由于static成员变量属于类本身,所有对象共享同一份;
而普通成员变量的缺省值是对象初始化时使用的规则,每创建一个对象都会根据这个规则初始化自己的成员变量。因此 static 成员变量不适合使用普通成员的缺省初始化方式。
3.2 static成员函数
3.2.1 static 成员函数的特点
- 普通成员函数:属于类,但是依赖对象执行,所以有 this 指针。
- static 成员函数:属于类,但是不依赖对象,所以没有 this 指针。
3.2.2 static成员函数和非static成员函数
static成员函数因为没有this指针,所以不能使用除了static成员变量的其他成员变量,同时也不能使用非static成员函数。
但是非static成员函数可以使用static成员变量和static成员函数。
3.2.3 static成员函数的类外访问方式
和static成员变量一样,static成员函数也是有两种访问方式,A::Get();也可以A a; a.Get();
作为成员,static成员也是受访问限定符的限制的。
4 友元
4.1 友元函数
#include<iostream>
using namespace std;
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
在函数声明前面加上一个friend放入类中就成为了这个类的友元函数。
4.1.1 友元函数的作用
友元函数可以访问该类的私有,保护成员,但是它不是成员函数,也没有this指针。
4.1.2 友元声明
友元声明可以在类的任意位置,不受访问限定符的限制。
4.1.3 友元函数的一对多
一个函数可以是多个类的友元,即我可以是A的朋友,也可以是B的朋友。
4.2 友元类
#include<iostream>
using namespace std;
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}
4.2.1 友元类的作用
友元类可以访问外部类的私有和保护成员,即是友元类中的所有成员函数都是外部类的友元函数,都可以访问外部类的私有和保护成员
4.2.2 友元类没有双向性
友元类是外部类的朋友,可以访问外部类的私有和保护成员,但是如果友元类中没有写外部类是它的朋友,那么外部类不能访问友元类的私有和保护成员。
类比就是你是我的朋友,你可以来我家玩,但是可能你可能没有把我当成朋友,可能不让我去你家玩。
4.2.3 友元类没有传递性
A是B的友元类,B是C的友元类,不可以说A是C的友元。
4.2.4 友元类的问题
如果过多使用友元类会导致耦合性太强,可能我这里用了你的成员,你这里用了我的成员,导致我改一个类的时候,很有可能另一个类使用了我改的这个部分,导致还需要再改这个类才能保证运行正确。
当然同时也会导致封装性被破坏,因为友元类不就是可以访问其他类的私有和保护成员吗?过多使用导致访问限定符用处减少,封装性被破坏。
5 内部类
#include<iostream>
using namespace std;
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;
}
5.1 内部类的特点
在计算外部类的大小时,是不会加上内部类的大小的,因为内部类是独立的类,受到的限制就是实例化内部类对象时会受到外部类类域的限制,如A::B b;,同时内部类也是受到内部类访问限定符的限制的。
5.2 内部和外部类的关系
内部类默认是外部类的友元, 但是外部类默认不是内部类的友元。(友元单向)
5.3 内部类的用处
内部类也是一种封装,如果A类和B类联系紧密,A类实现出来主要是给B类使用的,那么设计成内部类是比较合适的,并且如果不想内部类被外部使用,也可以用访问限定符来限制。
6 匿名对象
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(10) {
//...
return 1;
}
};
int main()
{
A aa1;
//A aa1();
A();
A(1);
A aa2(2);
Solution sol;
sol.Sum_Solution(10);
Solution().Sum_Solution(10);
return 0;
}
6.1 匿名对象和有名对象
有名对象就是A aa1;,匿名对象就是A(),有必要说明一点有名对象不可以A aa1();因为会和函数声明产生歧义,如void func(),匿名对象可以加上括号,因为不会产生歧义。
6.2 匿名对象的作用
匿名对象的作用其实就是让使用更加简单了。
Solution sol;
sol.Sum_Solution(10);
Solution().Sum_Solution(10);
本来要写两行的,现在只用写一行了
6.3 匿名对象和临时对象
匿名对象是临时对象的一种,生命周期在这一行,所以临时使用一下变量就可以用匿名对象。
7 对象拷贝时的编译器优化
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
int main()
{
A aa1;
f1(aa1);
cout << endl;
f1(1);
f1(A(2));
cout << endl;
cout << "***********************************************" << endl;
f2();
cout << endl;
A aa2 = f2();
cout << endl;
aa1 = f2();
cout << endl;
return 0;
}
7.1 两次连续构造
7.1.1 正常优化
A aa1; f1(aa1);先构造再拷贝构造,由于这里是在两行的,所以正常优化和激进优化都没有将这个优化掉。(但是不排除特别激进的编译器会优化掉,因为后面没有地方再使用aa1,可能编译器会依据这个来优化)f1(1);,先1隐式类型转换为临时对象(构造),临时对象再拷贝构造给aa,连续构造+拷贝构造,编译器之间优化为1构造aa。f1(A(2));,先构造匿名对象,匿名对象再拷贝构造aa,优化为2构造aa。
7.1.2激进优化
f2();,aa2先构造,再传值返回aa2,即aa2拷贝构造临时对象,其实这里不属于连续的构造加拷贝构造,因为是两行,正常优化编译器是不优化的,但是激进编译器还是会优化为构造(即不构造aa了,直接构造临时对象)
激进编译器就算你在中途改变了值,也是可以优化的,是比较激进的,但是结果还是正确的。
A f2()
{
A aa;
aa++;//如果重载了后置++
return aa;
}
7.2 三次连续构造
A aa2 = f2();,不优化是先构造aa,再拷贝构造临时对象,再拷贝构造aa2,正常优化是构造aa,再拷贝构造aa2,激进优化是直接构造aa2。aa1 = f2();,不优化就是构造,拷贝构造加上赋值重载,正常优化就是不优化的情况,激进优化是构造加赋值重载(直接构造临时对象了),赋值重载不算构造的一种,所以编译器没有优化。
—— 初始化列表、类型转换、static、友元、内部类、匿名对象与编译器优化&spm=1001.2101.3001.5002&articleId=164152451&d=1&t=3&u=6c2fbed6f9b943d99b0fa555792c1d6e)
1679

被折叠的 条评论
为什么被折叠?



