C++类和对象(三)
构造函数
class Date
{
public:
Date(int year, int month, int day)
{//赋初值
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
一个类尽量提供全缺省的默认构造函数。
初始化列表
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
pubilc:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
}
但是初始化列表不能解决所有的问题,一部分通过初始化列表,一部分通过函数体内初始化。注意:一个成员只能在初始化列表出现1次。
对象的每个成员什么时候定义初始化呢?-- 在初始化列表的时候
每个成员都要走初始化列表,就算不显式写在初始化列表,编译器也默认生成该列表,也会走这个列表。
如果在初始化列表显式写了,就用显式写的初始化列表。
如果没有在初始化列表显式初始化,那么
1、对于内置类型,有缺省值用缺省值,没有就用随机值;(缺省值的用处:就是用在初始化列表里!!)
2、对于自定义类型,调用默认它的默认构造函数,如果没有默认构造就报错。
优先级:初始化列表 > 缺省值 > 随机值
必须要初始化列表的原因:
//1、函数体内赋值无法解决const成员的初始化问题。函数体内的是赋值,而不是初始化。被const修饰的成员必须在定义的时候初始化,其他任何地方都不能修改
class B
{
public:
B()
{
_n = 10;//err 函数体内的叫做赋初值,而不是初始化
}
private:
const int _n; // const成员,必须在定义的时候初始化
};
----
//那这种情况下const成员就要采用列表初始化
class B
{
public:
B()
: _n(10)//列表初始化
{}
private:
const int _n; // const成员,必须在定义的时候初始化
};
------------------------
//2、自定义类型,且无默认构造函数时
//默认构造函数:1、不写,系统生成;2、全缺省;3、无参
class stack
{
public:
stack(int capacity = 4)//这个情况就是无默认构造函数,该构造函数不符合3者之一
: _top(0)
, capacity(capacity)
{
_a = (int*)malloc(sizeof(int)*capacity);
//do
}
private:
int* _a;
int _capacity;
int _top;
}
class B
{
public:
B(int capacity, int ref)
: _n(10)//const成员
, pushst(capacity)//类无默认构造函数的自定义对象
, ref(ref);//引用
{}
private:
const int _n; // const成员,必须在定义的时候初始化
stack pushst;
int& ref; //引用成员,必须在定义的时候初始化
};
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
- 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量;const成员变量;自定义类型成员(且该类没有默认构造函数时)。引用和const都是因为这两个数据类型都必须在定义时初始化。
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
explicit关键字
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//全缺省构造函数支持Date(2022);
//Date(int year, int month = 1, int day = 1)//半缺省构造函数支持Date(2022);
//Date(int year)//单参数构造函数支持Date(2022);
{
_year = year;
}
private:
int _year;
int _month;
int _day;
};
//拷贝构造的写法
Date d2(d1);
Date d2 = d1;//这句代码的实际意义也是拷贝构造
//由此联想到-单参数构造函数的写法
Date d1(2022);
Date d1 = 2022;//用2022生成一个Date类型的临时变量,然后再用临时变量去拷贝构造d1.这句代码涉及到隐式类型转换Date <-- int,隐式类型转换中间会生成一个临时变量,类型是Date
//同理
const Date& d2 = 2022;//d2引用的对象是这个临时变量。临时变量具有常性,且与原变量地址不同,故引用要加const
单参数构造函数/半缺省构造函数/全缺省构造函数的隐式类型转换:本来调用单参数构造函数会先构造一个隐式类型,再用这个隐式类型去拷贝构造。但是经过现在的编译器的优化,就会直接构造。
加上explicit关键字后,就不允许隐式类型转换。一般来说,是需要支持的。即对于单参构造函数来说,没有使用explicit修饰,具有类型转换作用,如果用explicit修饰构造函数,禁止类型转换(上述调用方式就会报错),当explicit去掉之后,代码可以通过编译。
**C++11才支持多参数构造函数。要用{}
初始化。**虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
class Date
{
public:
Date(int year, int month, int day)//多参数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
Date d1 = {2022, 12, 15};//构造+拷贝构造
//结果等价于Date d1(2022, 12, 15); //直接构造
const& Date d2 = {2022, 12, 15};
//以上的调用,如果构造函数用explicit修饰,都无法编译通过
验证VS2019优化了单参数的先构造再拷贝构造,变成直接构造。
类对象,不是构造出来的就是拷贝构造出来的。
int N = 0;
class A
{
public:
//构造
A(int a = 0)
:_a(a)
{
++N;
}
//拷贝构造
A(const A& aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
};
void Func1(A a)
{ }
void Func2(A& a)
{ }
A Func3()
{
A aa;
return aa;
}
A& Func4()//这里只是为了验证,实际上传引用返回是错的,因为传的是临时变量的引用,这个临时变量出了作用域就不存在了
{
A aa;
return aa;
}
int main()
{
//实现一个类,计算程序中创建出了多少个类对象
//对象 不是通过构造,就是通过拷贝构造实现
A a1(1);
A a2 = 2;
A a3 = a1;
cout << N << endl;//3
Func1(a1);//传值传参要调用拷贝构造
cout << N << endl;//4
Func2(a1);//传引用传参
cout << N << endl;//4
Func3();//传值返回
cout << N << endl;//6
Func4();//传引用返回
cout << N << endl;//7
}
//输出N=3,说明A a2 = 2;这句代码 是直接构造形成类对象。本来应该是先构造再拷贝构造。本应该是4个对象。
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。int A::N = 0;//类外定义并初始化静态成员,需要指定类域
静态成员属于类,并且被类的每个对象共享。
上述验证编译器优化的代码,使用了全局变量N,其实这是不好的,谁都可以访问且修改它。改进:可以把它写进class类中,用static修饰,会影响链接属性,使其生命周期变成全局的,且可以用访问限定符来限制访问,所有类对象使用一个静态变量,存在静态区。类内的静态就受到类域的限制。
静态变量分为:局部、全局、类内,三种的生命周期都是全局的,局部和类内的区别就是作用域的区别。
静态成员函数:没有隐含的this指针,可以在类外通过类域直接访问,而不是创建对象再通过对象去访问。一般是为了去访问静态成员变量而定义的,与静态成员变量配合使用。静态成员函数只能访问静态成员,不能访问非静态成员。
静态成员特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,不能直接在类外访问,要用静态函数去访问;
- 类静态成员即可用
类名::静态成员
或者对象.静态成员
来访问; - 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制.
习题:求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
这道题是static成员的最好例子。
有一个题目,要求类对象只能在栈开辟和存在,不允许在静态区和堆区。(单例模式,即全局只允许有1个对象)
A a1;//允许
static A a2;//不允许
A* pa = new A;//不允许
--------------
class Date
{
public:
static A GetObj(int a = 0)
{
A a_obj(a);
return a;
}
private:
int _a;
}
int main()
{
A a = A::GetObj(10);
return 0;
}
友元
友元函数
在12月11日的上课笔记中有。具体例子是重载operator<<和重载operator>>两个运算符。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
说明:友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰;友元函数可以在类定义的任何地方声明,不受类访问限定符限制;一个函数可以是多个类的友元函数;友元函数的调用与普通函数的调用原理相同。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
说明:1、友元关系是单向的,不具有交换性。(比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。)2、友元关系不能传递。(如果C是B的友元, B是A的友元,则不能说明C时A的友元。)3、友元关系不能继承。
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。这两个相当于是独立的类,只不过嵌套而已。内部类收到外部类的类域和访问限定符的限制。如果仅限外部类使用,那就把内部类设为private。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
匿名对象
A();//我们可以这么定义匿名对象,匿名对象的特点不用取名字,但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
//优化场景1
A aa1 = 1;//直接构造A(int a)(int型隐式转换成A类的对象)+拷贝构造A(const A& aa)==>优化 直接构造A(int a),即A aa1(1);
//优化场景2
A aa2;//直接构造
f1(aa2);//拷贝构造--不能优化,因为这是2句代码
//优化场景3
f1(A(1));//直接构造匿名对象A(int a)+拷贝构造A(const A& aa)==>优化 直接构造
//优化场景4
A ret = f2();//构造+拷贝构造+拷贝构造==>构造+拷贝构造。传值返回时,要先拷贝构造一个临时的esp,再将这个临时的拷贝构造给ret
//5
f2();//直接构造+拷贝构造--不能优化
-------
//综上启发 -- 极致优化
A f2()
{
return A(10);
}
ret = f2();//构造(合三为一)
课件里是这样的
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
A(int a)
~A()
----------------
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
A(int a)
~A()
----------------
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
A(int a)
A(const A& aa)
~A()
~A()
----------------
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
A(int a)
A(const A& aa)
~A()
A& operator=(const A& aa)
~A()
~A()