一.在谈构造函数
因为成员变量是类对象的一部分,所以在对对象定义的同时成员变量也就整体定义了。这个时候需要调用构造函数来完成对成员变量的初始化,但是一个对象中有多个变量,那么每个成员变量到底在什么地方进行初始化?
在构造函数中对类对象的成员变量有两种初始化方式:
- 构造函数体赋值
- 初始化列表
1.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初始化列表
初始化列表:是对象成员定义的地方,以一个冒号开始,每个"成员变量"后面跟一个放在括号中的初始值或表达式,接着是一个以逗号分隔的数据成员列表。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
1.3只能使用初始化列表的情况
1.3.1const成员变量和引用成员变量
const成员变量和引用成员都必须在定义的时候进行初始化,若是使用函数体赋值,那只是赋值并不是初始化。因此只能使用初始化列表进行初始化。
class B
{
public:
B(int a, int& ref)
:_ref(ref)
, _n(a)
{}
private:
int& _ref;
const int _n;
};
int main()
{
int n = 20;
B bb(10, n);
return 0;
}
1.3.2自定义类型成员(且该类没有默认构造函数时)
- 若我们在类中定义一个自定义类型的成员变量,当我们对对对象进行初始化的时候,自定义类型的成员会自动去调用它的默认构造函数进行初始化。
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
: _ref(ref)
, _n(a)
{}
private:
A _aobj;
int& _ref;
const int _n;
};
int main()
{
int n = 20;
B bb(10, n);
return 0;
}
- 但是当自定义类型中并没有定义默认构造函数的时候,此时就需要传参调用来对自定义类型进行初始化。但是我们在构造函数体赋值中并不能调用,因此只能在初始化列表来完成初始化。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_aobj(a) //调用显式定义的构造函数初始化
,_ref(ref)
, _n(a)
{}
private:
A _aobj;
int& _ref;
const int _n;
};
int main()
{
int n = 20;
B bb(10, n);
return 0;
}
注意: 初始化列表和函数体赋值一样,对内置类型不做处理(可设置缺省值,给初始化列表进行初始化),自定义类型调用它的构造函数。
1.4初始化列表和构造函数体赋值的不同
1.4.1声明的顺序影响执行的顺序
- 构造函数体赋值于声明的顺序无关,在构造函数体内逐语句执行。
- 初始化列表是按照声明的顺序执行的。
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();
}
解释:因为声明的顺序是_a2然后是_a1,因此在初始化列表中,也是这个顺序去进行初始化。
1.4.2函数体的分工
- 构造函数体赋值
都在函数体内完成
class stack
{
public:
stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc failed");
return;
}
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
- 初始化列表
在初始化列表进行初始化,在初始化列表中不方便做的,可以放在函数体中来做。
class stack
{
public:
stack(int capacity = 10)
:_a((int*)malloc(sizeof(int)* capacity))
,_top(0)
, _capacity(capacity)
{
//两个地方都可以进行初始化
//_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc failed");
exit(-1);
}
//对数组进行初始化
memset(_a, 0, sizeof(int) * capacity);
}
private:
int* _a;
int _top;
int _capacity;
};
总结:建议尽量使用初始化列表初始化,因为因为不管你是否使用初始化列表,都会先使用初始化列表初始化。对于自定义类型成员变量,则会去调用它的默认构造。
二. explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
2.1隐式类型转换
class A
{
public:
A(int x = 5)
:_a(x)
{
cout << "A(int x = 5)" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
int main()
{
A aa1 = 2;
return 0;
}
分析:A aa1 = 2;会将整型转换为自定义类型。首先2会去调用构造函数,构造一个A类型的临时对象,在用临时对象去拷贝构造aa1(构造 + 拷贝构造)。
此时我们发现,编译器并没有像我们分析的那样,先去调用构造函数,再去调用拷贝构造函数。那么我们在来看这个列子:
int main()
{
//“初始化”: 无法从“int”转换为“A &”
//A& aa2 = 2;
const A& aa2 = 2;
return 0;
}
分析:因为发生隐式类型转换回产生临时变量,临时变量具有常属性。 因此就验证了整型转换为自定义类型会调用构造和拷贝构造函数。当时在这个过程中,因为连续的调用构造函数和拷贝构造函数,所以编译器做了优化。
2.2阻止隐式类型转换
隐式类型转换有时候会让我们使用起来更方便些,若我们不想发生隐式类型转换,可以在构造函数前加explicit关键字。
class A
{
public:
explicit A(int x = 5)
:_a(x)
{
cout << "A(int x = 5)" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
int main()
{
// class“A”的构造函数声明为“explicit
//“初始化”: 无法从“int”转换为“A”
//A aa1 = 2;
return 0;
}
三.static成员
3.1概念
声明为static的类成员称为类的静态成员,静态成员只要突破类域和访问限定符的的限制就可以在类外访问。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量在类中声明,在类外面定义(可突破一次私有)。
3.2static成员的特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
3.3 练习题
3.3.1实现一个类,计算程序中创建出了多少个类对象
-
- 使用全局变量
int _scount = 0;
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
private:
};
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ << ':' << _scount << endl;
return ;
}
int main()
{
cout << __LINE__ << ':' << _scount << endl;
A aa1;
Func();
Func();
return 0;
}
但是使用全局有一个劣势,在任何地方全局变量都可以被改变。
void Func()
{
static A aa2;
cout << __LINE__ << ':' << _scount << endl;
_scount++;
return ;
}
-
- 使用静态成员变量
除了使用全局变量,我们还可以在类中定义静态成员变量,将静态成员变量封装在类中。
成员变量和静态成员变量的区别:
成员变量:属于每一个类对象,存储在对象里面。
静态成员变量:类的每一个对象共享的,存储在静态区。
注意: 静态成员变量不可以给缺省值。因为缺省值是给初始化列表的,静态成员变量没有初始化列表(初始化列表是对象成员定义的地方,静态成员变量并不在对象中)因此 静态成员变量只能在类中声明,在类外定义。
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
private:
static int _scount;
};
//类中声明,类外定于
int A::_scount = 0;
问题1:封装之后,此时静态变量是私有的在类外不可以直接访问。
解决方法:通过公有的静态成员函数返回静态成员变量。
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetCount()
{
return _scount;
}
private:
static int _scount;
};
//类中声明,类外定义
int A::_scount = 0;
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ << ':' << A::GetCount() << endl;
return;
}
int main()
{
cout << __LINE__ << ':' << A::GetCount() << endl;
A aa1;
Func();
Func();
return 0;
}
好处:此时通过对静态成员变量的封装,在类外就不能被随便被改变,可以更规范的对数据进行管理。
那么为什么将成员函数设为静态?因为:静态的成员函数没有this指针,有无对象皆可访问,没有对象时只需要指定类域和访问限定符就可以访问。
3.3.2 求1+2+3+…+n
class sum
{
public:
sum()
{
_ret += _i;
_i++;
}
static int GetCount()
{
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) {
sum s[n]; //创建N个对象,调用N次构造函数
return sum::GetCount();
}
};
3.3.3 设计一个类,在类外只能在栈或者堆上创建对象。
- 若不加限制,在栈,堆,静态区都可以
class A
{
public:
private:
int _a = 1;
int _b = 2;
};
int main()
{
A aa1; //栈
static A aa2; //静态区
A* ptr = new A; //堆
return 0;
}
- 若只能在栈或者堆上创建对象
此时我们发现无论是在什么地方创建对象,都会调用构造函数。我们就可以把构造函数私有化,然后通过一个静态的公有的函数来返回需要创建的对象
class A
{
public:
static A AGetStackObj()
{
A aa;
return aa;
}
static A* AGetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a = 1;
int _b = 2;
};
int main()
{
A::AGetStackObj();
A::AGetHeapObj();
return 0;
}
注意:
- 静态成员函数不可以访问非静态的成员。因为非静态的成员调用需要this指针而静态成员函数没有this指针。
- 非静态的成员可以调用静态成员。因为静态成员函数只要指定类域和访问限定符就可以访问且在类不受限制。。
- 静态成员变量和静态成员函数一般是一起配合使用。
三.友元
友元提供了一种突破封装的方式,通过这种方式我们可以在类外面访问类中的成员。但是却会增加会增加耦合度(列如通过友元可以改变类的成员变量)会破坏封装性,所以友元不宜多用。
3.1友元函数
之前的博客中我们有使用过友元函数:
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "年" << d._month << "月"
<< d._day << "日" << endl;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
3.1.1友元函数的特性
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(没有this指针)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2友元类
在A类中声明一个友元类B,则友元类B的所有成员函数都可以是A类的友元函数,都可以访问A类中的非公有成员。
class Time
{
//声明Date类是Time类的友元
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问Time类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
3.2.1友元类的特性
- 友元关系是单向的,不具有交换性。(在Time类中声明Date类为其友元类,那么Date类可以访问Time类的私有成员变量,但是反过来却不行)
- 友元关系不能传递。(如果C是B的友元, B是A的友元,则不能说明C是A的友元)
- 友元关系不能继承。
四.内部类
4.1概念
普通类是定义在全局,而内部类则是定义在一个类的内部。
内部类是一个独立的类,它不属于外部类。因此外部类的对象不能去访问内部类的成员,外部类对内部类没有任何的访问权限。
内部类是外部类的友元类。因此内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。因此建议将成员变量都定义在外部类,此时外部类和内部类都可以访问
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A()); //A()是一个匿名对象
return 0;
}
4.2内部类的特性
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- 若内部类不在外部类中定义,sizeof(外部类)就等于外部类的大小,和内部类没有任何关系。
-
- 内部类不在外部类中定义
class A
{
public:
class B
{
private:
double d = 3.14;
};
private:
int _t = 0;
int _h = 0;
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(A::B) << endl;
return 0;
}
- 2.内部类在外部类中定义
class A
{
public:
class B
{
private:
double d = 3.14;
};
private:
int _t = 0;
int _h = 0;
B b; //内部类在外部类中定义
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(A::B) << endl;
return 0;
}
- 内部类受访问限定符的限制。
-
- 若内部类是公有的,在类外面,只需要指定外部类::内部类就可以创建对象。
-
- 若内部类是私有的,在类外面不能直接创建对象。
五.匿名对象
匿名对象就是没有名字的对象。它在使用上和有名对象有一些区别。
匿名对象调用函数时,需要指定类域。类::类().函数名。匿名对象和有名对象一样传参,可以在()向构造函数传参进行初始化,。
class A
{
public:
A(int x = 2)
:_a(x)
, _b(x)
{
cout << "A(int x = 2)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A(const A& a)
{
_a = a._a;
_b = a._b;
cout << "A(const A& a)" << endl;
}
void Print() const
{
cout << _a << ' ' << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
//有名对象
A aa1(3);
aa1.Print();
//匿名对象
A(3);
//匿名对象调用函数
A::A(3).Print();
return 0;
}
5.1匿名对象的特性
-
匿名对象即用即销毁,生命周期在当前行。
-
匿名对象具有常性
int main()
{
//“初始化”: 无法从“A”转换为“A &”
//A& ra = A(1);
const A& ra = A(1);
return 0;
}
- const引用可以延长匿名函数的生命周期
六.拷贝对象时的一些编译器优化
这里以VS2022举列
- 优化的情况
class A
{
public:
A(int x = 2)
:_a(x)
, _b(x)
{
cout << "A(int x = 2)" << endl;
}
A(const A& a)
{
_a = a._a;
_b = a._b;
cout << "A(const A& a)" << endl;
}
private:
int _a;
int _b;
};
A Func1()
{
A aa1;
return aa1;
}
void Func2(A aa)
{
}
int main()
{
//构造 + 拷贝构造 ->优化为构造
A ra1 = 2;
//构造 + 拷贝构造 ->优化为构造
Func2(A(1));
Func2(1);
//拷贝构造 + 拷贝构造 ->优化为拷贝构造
A ra2 = Func1();
return 0;
}
- 不会优化的情况
int main()
{
A aa1; //构造
Func2(aa1); //拷贝构造 ->不会优化
return 0;
}
总结:当在一行的一个表达式中连续出现构造或者构造加拷贝构造时,编译器会对它进行优化。