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; //引用成员,必须在定义的时候初始化
};

【注意】

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量const成员变量自定义类型成员(且该类没有默认构造函数时)。引用和const都是因为这两个数据类型都必须在定义时初始化。
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

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指针,可以在类外通过类域直接访问,而不是创建对象再通过对象去访问。一般是为了去访问静态成员变量而定义的,与静态成员变量配合使用。静态成员函数只能访问静态成员,不能访问非静态成员。

静态成员特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,不能直接在类外访问,要用静态函数去访问;
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问;
  4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员
  5. 静态成员也是类的成员,受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。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. 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()
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值