目录
- 一、再谈构造函数
-
- [1.1 初始化列表](#1.1 初始化列表)
-
- [1.1.1 初始化列表写法](#1.1.1 初始化列表写法)
- [1.1.2 哪些成员要使用初始化列表](#1.1.2 哪些成员要使用初始化列表)
- [1.2 初始化列表的特点](#1.2 初始化列表的特点)
-
- [1.2.1 队列类问题解决](#1.2.1 队列类问题解决)
- [1.2.2 声明顺序是初始化列表的顺序](#1.2.2 声明顺序是初始化列表的顺序)
- [1.3 explicit关键字](#1.3 explicit关键字)
-
- [1.3.1 explicit关键字的作用](#1.3.1 explicit关键字的作用)
- 二、static成员
-
- [2.1 类的静态成员概念](#2.1 类的静态成员概念)
- [2.2 类里创建了多少个对象问题](#2.2 类里创建了多少个对象问题)
- 三、友元
-
- [3.1 概念](#3.1 概念)
- [3.2 友元函数](#3.2 友元函数)
- [3.3 友元类](#3.3 友元类)
- 四、内部类
- 五、拷贝对象时的一些编译器优化
一、再谈构造函数
1.1 初始化列表
构造函数之前我们已经学过大部分内容,但是并没有学全,还有一个很重要的东西------初始化列表
1.1.1 初始化列表写法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
cpp
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
}
//
Date d1(2023, 11, 9);
运行结果:
括号里也可以是数值(没有传参的情况):
cpp
Date()
:_year(2023)
, _month(11)
, _day(9)
{
}
补充:声明给缺省值其实是给初始化列表的,但是在没显示写构造函数的情况;如果有写构造函数的话用构造函数中定义的值,构造函数没有参数或者具体数值就是随机值。(VS下随机值是0)
1.1.2 哪些成员要使用初始化列表
先总结下,再逐一分析
一定要初始化列表的成员有:
1.引用成员变量
2.const成员变量
3.没有默认构造的自定义类型成员变量
cpp
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
,t(year)
,a(5)
{
}
private:
int _year;
int _month;
int _day;
int& t;//引用成员变量
const int a;//const成员变量
};
运行结果:
1️⃣使用引用有一个规定,那就是必须初始化。而在以上没有初始化是因为这些都是声明 ,接下来要定义这个引用成员变量,那么定义的地方在哪?就在初始化列表。
2️⃣const成员变量与引用也是一样的,在私有的区域里只是声明,要定义的话必须在初始化列表。
3️⃣上篇文章使用队列类的时候我们没有写队列类的构造函数,内置类型声明有缺省值使用缺省值,没有是随机值;自定义类型成员变量让编译器自动调用它的默认构造。
复习下什么是默认构造:
1.我们不显示写构造函数编译器默认自动生成的
2.没有传参的
3.全缺省的
如果我们要自己控制参数为多少 ,也就是说要传参给自定义类型的构造函数(或者自定义类型的构造函数不是默认构造,就要我们自己传参 ,否则我们既没有显示队列类的构造函数,也没有把它的自定义类型的构造为默认构造,结果就为随机值),就必须显示写构造函数初始化,构造函数要有初始化列表 。
总结一下:
引用成员变量和const成员变量必须要在定义的地方初始化,这个地方在初始化列表;自定义类型成员变量不写这个类的构造函数,自动调用这个自定义类型成员的默认构造;写这个类的构造函数,下面接着分析~~
1.2 初始化列表的特点
1.2.1 队列类问题解决
之前我们不写队列类的构造函数,这次就要写,构造函数里初始化自定义类型的成员变量,要用初始化列表解决。
以下代码:
cpp
class Stack
{
public:
Stack(int capacity = 3)
{
cout << "Stack(int capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = NULL;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class Queue
{
public:
// 队列类的构造函数
Queue()
:_st1()
,_st2()
,_size()
{
}
private:
Stack _st1;
Stack _st2;
int _size;
};
int main()
{
Queue q1;
return 0;
}
参数是谁,有几种情况:
1️⃣只有构造函数,没有参数、括号后没有具体值
通过调试查看允许结果:
我们发现是0、3、0、3、0,所以在没有传任何值的情况下,初始化列表里对于内置类型是随机值(VS下变成0),自定义类型调用它的默认构造
2️⃣没有参数、括号后有具体值
cpp
Queue()
:_st1(10)
, _st2(20)
, _size(30)
{
}
调试结果:
3️⃣有参数为全缺省、括号后有具体值
cpp
Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(1)
, _st2(2)
, _size(3)
{
}
调试结果:
4️⃣有参数为全缺省、括号后为参数
cpp
Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(st1)
, _st2(st2)
, _size(size)
{
}
调试结果:
5️⃣自己传参
cpp
Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(st1)
, _st2(st2)
, _size(size)
{
}
///
Queue q1(11, 77, 99);
调试结果:
6️⃣没有构造函数,声明给缺省值
cpp
private:
Stack _st1 = 7;
Stack _st2 = 8;
int _size = 9;
};
调试结果:
总结一下:
1.当自定义类型成员的构造函数不是默认构造,有以下 :自己传参5️⃣;有参数为全缺省、括号后为参数4️⃣;有参数为全缺省、括号后有具体值3️⃣;没有参数、括号后有具体值2️⃣;声明给缺省值6️⃣。总之就是有给自定义类型成员的构造函数传参 。
有具体值就用具体值,没有具体值自己传参优先,其次缺省值
为了方便控制参数,所以使用5️⃣较好。
2.当自定义类型成员的构造函数是默认构造:只有构造函数、没有参数、没有具体值1️⃣
还有第七种7️⃣不写构造函数(上篇文章的写法)
这两种其实没有区别,所以干脆不写构造函数。
1.2.2 声明顺序是初始化列表的顺序
直接用例子展示:
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;//day放在最前面
int _year;
int _month;
};
int main()
{
Date d1(2023, 11, 9);
d1.Print();
return 0;
}
调试结果:
所以天最先被初始化,其次是年和月。
1.3 explicit关键字
1.3.1 explicit关键字的作用
对于只有内置类型的单参数的构造函数,具有类型转换的功能
看以下代码:
cpp
class A
{
public:
A(int a)
{
_a = a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A aa(1);
aa = 3;
aa.Print();
return 0;
}
aa是自定义类型,3是整型,运行结果:
如果不想转换发生,构造函数前加explicit,此时编译器会有报错提示。
补充:加explicit关键字可以阻止隐式类型转换,但不能阻止强转。
没有使用explicit修饰,多参数且是半缺省也支持。
cpp
class Date
{
public:
Date(int year, int month = 2, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = (2023, 11, 10);
return 0;
}
调试结果:
注意:小括号是逗号表达式,只算最后一个数,其他的是缺省值。
修改一下,把小括号变成花括号,并且不要缺省值:
cpp
Date d1 = { 2023, 11, 10 };
调试结果:
二、static成员
2.1 类的静态成员概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
2.2 类里创建了多少个对象问题
定义一个变量count用来计数,每创建(构造)一个对象,或者拷贝构造一个对象,count++。我们先用全局变量count来计数,看会发生什么:
1️⃣
cpp
int count = 0;
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() { }
private:
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa;
Func();
cout << count << endl;
return 0;
}
编译器报错了,提示count为不明确的符号,说明命名冲突了。
命名冲突的解决办法------命名空间
2️⃣
cpp
namespace yss
{
int count = 0;
}
class A
{
public:
A() { ++yss::count; }
A(const A& t) { ++yss::count; }
~A() { }
private:
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa;
Func();
cout << yss::count << endl;
return 0;
}
运行结果:
这个结果确实是我们要的答案,但是这里却有一个问题:如果再多一个类,使用全局变量就不能把两个类区分开来,导致计数完一个类再去计数另一个类count的值没有更新,也就是说两个类的对象是合并在一起的。
我们换一种方式,定义类的成员变量
3️⃣
cpp
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() { }
//private:
int count = 0;
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa;
Func();
cout << aa.count << endl;
return 0;
}
我们暂时把私有的权限注释掉,看看运行结果如何:
为什么会是1呢?因为每创建一个对象,count+1,但是又创建一个对象,count清零再+1。说明类里一个对象一个count,而我们要的结果是类里的所有对象的个数,可是每个对象有自己的count。
要让类里的对象都是一个count怎么办,使用静态成员变量,static修饰count。
静态成员变量一定要在类外进行初始化
4️⃣
cpp
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() { }
//private:
static int count;
};
int A::count = 0;
A Func()
{
A aa;
return aa;
}
int main()
{
A aa;
Func();
cout << aa.count << endl;
return 0;
}
运行结果:
结果没问题了,但是,没有私有的权限,类的封装性就不能很好了。
改进:可通过一个成员函数来返回count的值
5️⃣
cpp
class A
{
public:
A() { ++count; }
A(const A& t) { ++count; }
~A() { }
int Getcount()
{
return count;
}
private:
static int count;
};
int A::count = 0;
A Func()
{
A aa;
return aa;
}
int main()
{
A aa;
Func();
cout << aa.Getcount() << endl;
return 0;
}
运行结果:
但是如果我们想通过调用Func函数来计数对象创建了几个,main函数里实例化的对象不算该怎么办?
假设调用两次Func函数,先别注释对象的实例化,得到的结果应该为5
cpp
int main()
{
A aa;
Func();
Func();
cout << aa.Getcount() << endl;
return 0;
}
然后把对象注释掉,发现编译器报错:未定义标识符aa。
可以使用静态成员函数 来解决
6️⃣
cpp
static int Getcount()
{
return count;
}
注意:要通过类名::静态成员来访问
运行结果:
总结:
静态成员变量和静态成员函数与全局变量和全局函数很像,只是受作用域限定符和点成员操作符限制
补充:
静态成员函数不可以 调用非静态成员函数,因为静态成员函数没有this指针; 非静态成员函数可以调用类的静态成员函数,因为它们属于同一个类。
三、友元
3.1 概念
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
3.2 友元函数
当一个函数不是类的成员函数,但又要能访问到类里的成员变量,必须使用友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
cpp
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//友元函数
friend ostream& operator<<(ostream& out, Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{
Date d1(2023, 11, 10);
cout << d1;
return 0;
}
特点:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
3.3 友元类
创建一个时间类,声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
cpp
class Time
{
// 友元类
friend class Date;
public:
Time(int hour = 2020, int minute = 6, int second = 5)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 10)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
Print();
}
void Print()
{
cout << _t._hour << "-" << _t._minute << "-" << _t._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
特点:
友元关系是单向的,不具有交换性 。在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递如果C是B的友元, B是A的友元,则不能说明C时A的友元。
友元关系不能继承
四、内部类
一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
内部类就是外部类的友元类。内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
cpp
class A
{
public:
class B
{
public:
void Func()
{
cout << _a << endl;
}
};
private:
int _b;
private:
static int _a;
};
int A::_a = 10;
int main()
{
A::B b1;
b1.Func();
return 0;
}
特点:
内部类可以定义在外部类的public、protected、private都是可以的
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
sizeof(外部类)=外部类,和内部类没有任何关系
五、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。不同的编译器优化的方式可能会不同。
1️⃣隐式类型,连续构造+拷贝构造->优化为直接构造
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
void Func(A aa)
{
}
int main()
{
Func(1);
return 0;
}
1是整型,发生隐式类型转换通过调用构造函数产生临时变量,临时变量再拷贝给形参调用拷贝构造函数。
2️⃣一个表达式中,连续构造+拷贝构造->优化为一个构造
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
void Func(A aa)
{
}
int main()
{
Func(A(1));
return 0;
}
在一个表达式中,A(1)调用构造函数产生临时变量,临时变量再拷贝给形参调用拷贝构造函数,优化成一次构造。
3️⃣一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa2 = Func();
return 0;
}
实例化aa2调用一次构造,进入Func()函数,创建aa对象调用一次构造,两次构造优化为一次构造;局部变量拷贝给返回值调用拷贝构造,返回值拷贝给aa2再调用拷贝构造,两次拷贝构造优化成一次拷贝构造。
4️⃣一个表达式中,连续拷贝构造+赋值重载->无法优化
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa3;
aa3 = Func();
return 0;
}
实例化aa3调用构造,此时aa3已存在,调用Func()的返回值是赋值,Func()函数里创建aa对象调用构造,局部变量拷贝给返回值调用拷贝构造,最后赋值。