目录
一、构造函数的初始化
1. 构造函数体赋值
在创建对象时,编译器都过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
那么真正的初始化在哪里呢?
2. 初始化列表
初始化列表是构造函数的一部分
2.1 概念
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
const 与 &(引用)共同特征就是都必须在定义时初始化。在类内,成员变量只是声明,在对象实例化时,才对对象整体定 义开空间,而对象的成员定义的地方就在构造函数处——初始化列表(构造函数内不是定义,是赋值,真正的定义在初始化列表)
对于自定义类型,如果没有在初始化列表初始化,那么编译器会自动调用其自身的构造函数进行初始化,而对于内置类型,如果没有在初始化列表初始化,那么其值是随机的,编译器对其不做处理,所以C++11新增:对类内成员函数的声明处,允许给缺省值,就是为了初始化时给初始化列表;如果在初始化列表处对内置类型初始化,那么缺省值就不管用了
回顾:
默认构造函数:编译器自动生成的、自己写的全缺省的、无参数的,共三种
当该类没有默认构造函数,就意味着自己手写的构造函数不是全缺省参数,需要传参,所以如果没有在初始化列表初始化,那么编译器就会不知道如何初始化该类的成员变量,这就是为什么如果该类没有默认构造函数就必须要在初始化列表初始化的原因
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a = 0)" <<endl;
}
private:
int _a;
};
class B
{
public:
// 初始化列表:对象的成员定义的位置
B(int a, int& ref)
:_ref(ref)
,_n(1)
,_x(2)
,_aobj(10)
{
//_n = 0;
//_ref = ref;
}
private:
// 声明
A _aobj; // 没有默认构造函数
// 特征:必须在定义的时候初始化
int& _ref; // 引用
const int _n; // const
int _x = 1; // 这里1是缺省值,缺省值是给初始化列表的
};
int main()
{
int n = 10;
// 对象整体定义
B bb1(10, n);
//B bb2(11, 2);
return 0;
}
那么,我们是否能使用初始化列表完全取代构造函数体内赋值的操作取代了呢?
答:不能,因为如果我们malloc空间后,返回的是指针,我们是需要进行判空的,该操作在初始化列表中,是很不方便的编写的,此外memset等函数或操作都不能很方便的写在初始化列表中,总有一些操作是初始化列表做不了的
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。所以,我们在初始化时尽量用初始化列表,当遇到额外的不方便的操作,放在构造函数体内即可
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
}
int main()
{
A aa(1);
aa.Print();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
答案为D, 初始化列表中的初始化顺序与成员变量在类内的声明顺序相同,此题中先初始化_a2,再初始化_a1,所以_a2为随机值,_a1为1
在构造函数体内,赋值顺序与语句有关,不受此影响
如果我们在类中的变量声明处给成员变量一缺省值,那么该成员变量会在构造函数的初始化列表中被初始化为缺省值(缺省值已经给出,在初始化列表处必然会被初始化为缺省值,这是我们不能改变的),但是当我们实例化对象的时候不想用这一缺省值时,我们可以在构造函数内进行赋值修改这一成员变量的值
2.2 单参数构造函数的隐式类型转换式构造
我们来看一个隐式类型转换的构造,针对单参数的构造函数
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2;
//int i = 10;
//double d = i;
return 0;
}
·这里的 A aa2 = 2是隐式类型转换,将2整形转换成自定义类型A,类似于int型变量i赋值给double型变量d,这其中会发生隐式类型转换,产生一个临时变量存储double型i,再将double型i赋值给d;同理,A aa2 = 2意思为:2构造一个A的临时变量,临时变量再拷贝构造aa2,但是编译器不能“容忍”效率低下的类型转换(一般对于连续的构造、拷贝构造,编译器都会优化),编译器优化为将 2 作为参数直接构造aa2
再来看另一种情况:
A& aa3 = 2;
错误:无法从“int” 转换为A& ,由于这里不是连续的构造和拷贝构造,它仅仅是构造,因为引用&不是拷贝,所以这里还是需要临时变量,又因为2在构造临时对象时,该临时对象具有常性,直接用引用类型接收,权限放大,出现错误,所以aa3要用const修饰
const A& aa3 = 2;
2.3 explicit关键字
有没有办法能不允许其隐式类型转换呢?这里有关键字explicit
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
private:
int _a;
};
使用explicit关键字加在构造函数前,主函数内的 A aa2 = 2 将不再允许临时类型转换,程序报错
二、static静态成员
1. 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
面试题:实现一个类,计算程序中创建出了多少个类对象
我们正常想法可能是定义全局变量记录构造函数、拷贝构造函数调用次数,当调用析构函数时,全局变量值减一,最后输出大小
int _scount = 0;
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
/*static int GetACount() { return _scount; }*/
private:
static int _scount;
};
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ << ":" << _scount << endl;
// 全局变量的劣势:任何地方都可以随意改变
_scount++;
}
int main()
{
cout <<__LINE__<<":"<< _scount << endl; // 1
A aa1;
Func(); // 3
Func(); // 3
return 0;
}
__LINE__宏定义,输出当前代码执行到的行数 ,我们在22行的Func函数中添加了_scount++,最终的输出变为了4(由于静态变量存储在静态区,aa2第一次定义后变量没有销毁,在第二次调用该行代码时,并没有重复定义,所以正确答案_scount应为3,但是在认为修改了一小步后,结果出错),这就直接体现了全局变量的劣势所在:任何地方都可以随意改变。
所以我们将_scount封装在类中,使用static修饰,成为静态成员变量,所谓静态成员变量与成员变量的区别在于,成员变量属于每一个类对象,储存在对象里面,而静态成员变量属于类,属于类的每个对象共享,存储在静态区,所以不能在初始化列表中对静态成员变量进行初始化,因为它不属于某一对象的成员变量。
对于静态成员变量,在类外的全局位置进行初始化,虽然它是私有的,但是在全局初始化静态变量时,这一操作是被允许的。
且静态成员变量不能给缺省值,缺省值是给初始化列表的
int A::_scount = 0;
但是对于在类外使用 A::_scount 进行访问时就不允许了,会受到private的保护,如果是public那么就可以在类外使用 A::_scount 进行访问。
那么如何在类外访问private保护的静态成员变量呢?
- 构造一个成员函数 GetACount( ) ,将静态成员变量作为返回值
可是如果没有实例化对象,那么就不能使用成员函数进行访问,这时我们可以将该成员函数使用static修饰,使其成为静态成员函数,就可以在没有对象的情况下在类外进行访问静态成员函数,这是因为静态成员函数没有this指针,在类外指定了类域和访问限定符后就可以访问该函数,并且不能在静态成员函数内访问成员变量,因为想要访问成员变量都要用到 this 指针。
由于静态成员函数没有this指针,所以在其函数内不能调用其余成员函数,因为没有函数调用地址,但是其余的成员函数可以调用静态成员函数,因为有this指针且都在类中,相当于在一个“大家庭”,所以普通成员函数可以访问静态成员函数。
在类外能访问到静态成员变量是因为它属于类,不属于某一单独的对象。
cout << A::GetACount() << endl;
例题:
思路:使用n次构造函数并结合静态成员变量,即可巧妙的解决该问题
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int Get_ret()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n)
{
//这里涉及到了变长数组的概念,C++11允许变长数组
Sum a[n];
return Sum::Get_ret();
}
};
例题二:
设计一个类,使得在类外只能在栈或堆上创建对象
在类外,我们可以实例化栈、静态区、堆上的对象。但是此时我们加上了限制,如只能在栈上实例化对象,那么为了避免在其他内存空间创建对象,我们直接用private修饰类的构造函数,相当于“封死”外部任何形式的实例化,使得只能在类内实例化之后再返回实例化对象,但此时又出现一个问题,普通成员函数参数列表内默认有一个类的对象,this指向该对象,可是我们还没有创建对象,我们的目的就是创建对象,这就矛盾了,成为了“先有鸡还是先有蛋”的问题
此时,静态成员函数就登场了,由于静态成员函数没有this指针,就代表了它不需要参数,这就完美解决了上面的问题
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//static A aa1; // 静态区
//A aa2; // 栈
//A* ptr = new A; // 堆
A::GetStackObj();
A::GetHeapObj();
return 0;
}
2. 特性
三、友元
1. 友元函数
说明 :
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
四、内部类
1. 概念
特性:1. 内部类定义在外部类的 public、protected、private都是可以的。2. 注意内部类可以直接访问外部类中的 static、枚举成员,不需要外部类的对象/ 类名。3. sizeof( 外部类)= 外部类,和内部类没有任何关系
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
内部类受外部类的访问限定符限制:公有内部类、私有内部类
五、匿名对象
有名对象——生命周期在当前函数的局部域
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//有名对象
A aa(1);
//匿名对象
A(2);
return 0;
}
匿名对象即用即销毁,生命周期在当前行。
一般来说,我们要访问类的成员函数,都要先有对象,再利用对象访问成员,(用类型是不能访问成员的!) 当我们没有实例化对象时,我们就可以利用匿名对象来访问,例
class Solution {
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl;
//...
return n;
}
};
int main()
{
Solution s1;
s1.Sum_Solution(10);
Solution().Sum_Solution(20);
return 0;
}
若构造函数内需要传参,那么在匿名对象()内加上参数即可,匿名对象与有名对象的区别就在于有没有名字
1. const引用匿名对象
一般情况,匿名对象生命周期在当前行。因为匿名对象具有常性,我们用引用接收是错误的,权限放大,我们需要用const引用接收,这时我们再运行程序,会发现该临时对象生命周期改变,在局部域。这是为了防止野引用
//A& ra = A(1); //匿名对象具有常性
//const引用延长了匿名对象的生命周期
const A& ra = A(1);
2. 匿名对象的隐式类型转换
void push_back(const string& s)
{
cout << "push_back:" << s << endl;
}
int main()
{
//1
string str("11111");
push_back(str);
//2
push_back(string("222222"));
//3
push_back("222222");
return 0;
}
2是匿名对象,3是临时对象,两者相同都具有常性,所以形参都要有const修饰
void Func1(A aa)
{}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra1 = Func5(); // 拷贝构造+拷贝构造 ->优化为拷贝构造
cout << "==============" << endl;
A ra2;
ra2 = Func5();
//A aa1;
//Func1(aa1); // 不会优化
//Func1(A(1)); // 构造+拷贝构造 ->优化为构造
//Func1(1); // 构造+拷贝构造 ->优化为构造
//A aa2 = 1; // 构造+拷贝构造 ->优化为构造
return 0;
}
对于ra1,由两次的连续的拷贝构造(返回临时遍历),编译器自动优化为一次拷贝构造
对于ra2,由于是两行代码,构造不连续,编译器不会优化,会成为构造、构造、拷贝构造、析构、赋值运算符重载、析构
所以,对于能一行写完的对象之间的操作,尽量不写成两行,这样不仅繁琐,还会干扰编译器
总结
本节补充了类和对象的剩余细节,至此,我们较为完善的学习了C++类和对象。
类和对象的细节繁多,较为难学,根据文章内容的理解再查阅C++对应书籍,就会更加明了。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!