C++——类和对象(下)

目录

一、继续学习构造函数

1.初始化列表

2.explicit关键字

二、static成员

三、友元

1.友元函数

2.友元类

四、内部类(了解一下)

五、匿名对象

六、拷贝对象时的一些优化


一、继续学习构造函数

对象的初始化分为两部分

1.函数体内赋初值。只能叫赋初值的原因是:初始化只能初始化一次,而函数体内可以多次赋值。

2.初始化列表

1.初始化列表

形式如下:

冒号开始,中间隔开用逗号,括号里放初始值或表达式。

以栈的构造函数举例:

Stack(int capacity = 4)
		:_a((int*)malloc(sizeof(int)*capacity))
		,_top(0)
		,_capacity(capacity)
	{
		if (_a == nullptr)
		{
			perror("malloc fail\n");
			exit(-1);
		}
	}


    //或者这样混合着来也行。一部分在函数体内部,一部分在初始化列表
	Stack(int capacity = 4)
		: _top(0)
		, _capacity(capacity)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail\n");
			exit(-1);
		}
	}

1.成员变量初始化可以混合着来,一部分在函数体内部,一部分在初始化列表。

2.每个成员变量在初始化列表中只能出现一次。

3.每个成员都要在初始化列表初始化,就算不显示在初始化列表写,也要进初始化列表。

  • 内置类型如果在初始化列表显示写了,按你写的初始化;没有显示写,有缺省值用缺省值,没缺省值用随机值。(缺省值在定义成员变量的时候给)
  • 自定义类型在初始化列表没显示初始化,调默认构造(没有默认构造就报错);显示初始化调构造函数(这个构造函数不要求是默认构造)。

                      

               04983dbfc92e43e8a2970bc313d54c89.png 

                     所以成员变量的缺省值是在初始化列表用的 。构造函数参数列表的缺省值在构造函数体内部使用。

4.尽量用初始化列表初始化,因为你用不用都会先走初始化列表。

5.以下成员必须在初始化列表进行显式初始化

const,引用,没有默认构造函数的自定义类型


详细讲解第5点:

1.const成员变量只能用初始化列表进行初始化

原因:const变量必须在定义的时候就初始化,不然就报错。

 4c9cb40d5af249b4a10570b8170afe5a.png 类里面的成员变量都只是声明

Stack st1;//这才是类对象的定义。对象实例化是整体定义,并调用构造函数初始化。

对象的每个成员什么时候定义初始化?

在初始化列表定义初始化。

所以必须在定义的时候就初始化的成员,必须在初始化列表进行初始化。

比如现在说的const成员,就必须要在定义时就初始化,即必须在初始化列表初始化。

2.自定义类型成员变量,且没有它的默认构造函数

class A
{
public:
		A(int a)//A没有默认构造函数。默认构造函数:1.全缺省构造函数2.无参构造函数3.没写构造函数事,编译器自动生成的构造函数
		:_a(a)
		{}

private :
	int _a;
};


class B
{
public:
 
	B()
		:_n(10)
		, _m(2)
		, _aa(11)//【自定义类型,且没默认构造】
	{

	}

private:
	const int _n;  // 这里是声明。const成员,必须在定义的时候初始化
	int _m = 1;    // 初始化列表里,没显示写的时候,有缺省值用缺省值,没缺省值用随机值。

	A _aa;//A没有默认构造函数
};

3.引用成员变量

引用和const一样,只能在定义的时候初始化。

所以必须在初始化列表就初始化。


6. 按成员变量的声明顺序在初始化 列表里初始化。

例:

class A
{
public:
	A(int a)//初始化列表里先初始化_a2再_a1
		:_a1(a)
		,_a2(_a1)
	{}

	void Print()
	{
		cout <<"_a1="<< _a1 << " " <<"_a2="<< _a2 << endl;
	}

private:
	int _a2;
	int _a1;
};

int main()
{
	A aa(1);
	aa.Print();
	return 0;
}

6888d4752dd3421f90669121132a5cf6.png

2.explicit关键字

读代码:

//【隐式类型的转换】
class Date
{
public:
	Date(int year)
		:_year(year)
	{}
//日期类的拷贝构造可以不写

private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	//之前引用说过
	
    int a = 0;

	double b = a;//隐式类型转换,先给临时变量。普通变量的赋值不考虑权限。
	
    const double& c = a;//引用要考虑权限。隐式类型转换,产生临时变量有常性,权限不能放大,所以加const

	Date d1(2022);//调构造函数

	//隐式类型的转换

	Date d2 = 2022;//单参数的构造函数。支持用一个参数构造一个Date类。用2022构造临时对象,再由临时对象拷贝给都d2

	const Date& d5 = 2022;//引用的时候要注意权限的放大和缩小。2022先做临时对象的引用,临时变量具有常性,就像是const的,再拷贝构造

	Date d3(d1);//拷贝构造
	Date d4 = d1;//也是拷贝构造


	return 0;
}

Date d2 = 2022;//理论上是先构造,后拷贝构造。现在的编译器已经优化成直接构造了。

const Date& d5 = 2022;//引用也是先构造,后拷贝构造。

单参数,全缺省的构造函数都可以隐式类型转换。

多参数构造函数也能隐式类型转换(c++11拓展支持了,老版本不支持)

class Date
{
public:
	Date(int year,int month ,int day)
		:_year(year)
		,_month(month)
		,_day(day)

	{}


private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1 = 2022, 10, 26;//no
	Date d2 = (2022, 10, 26);//no
	Date d3 = { 2022,10,26 };//先构造后拷贝
	Date d4(2022, 10, 26);//直接调构造函数
	const Date& d5 = { 2022,10,26 };//必须加const

	return 0;
}

2056dff34d3b4697be6e7edf0ab85cb0.png

 如果允许隐式类型转换就不加explicit,不想允许隐式类型转换,构造函数加explicit。

如下:

09f3351d24bb440eba79c2ce88e13607.png

5cbc9fabfb4b44399141c0135214a858.png

 二、static成员

类对象的传引用传参、传引用返回 和 传值传参、传值返回会不会调构造函数?

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 F1(A aa)
{

}

void F2(A& aa)
{

}

A F3()
{
	A aa;
	return aa;
}

A& F4()
{
	A aa;//调一次构造函数
	return aa;//虽然不能引用返回,因为出了作用域aa会销毁,拿到个随机值。但是编译器不会报错,不做强制检查
}



int main()
{
	A aa1(1);//构造

	A aa2 = 2;//编译器优化,直接构造

	A aa3 = aa1;//拷贝构造

	cout << N << endl;//输出3

	F1(aa1);//类对象传值传参调拷贝构造
	cout << N << endl;//输出4

	F2(aa1);//引用传参,不调拷贝构造
	cout << N << endl;//继续输出4

	F3();//传值返回类对象。函数体内构造一次,返回的时候拷贝构造给临时变量。如果再接收返回值,会再调一次拷贝构造。	cout << N << endl;//输出6

	F4();//传引用返回,函数体内部用了一次构造函数
	cout << N << endl;//输出7

	//类对象的传参和返回值尽量用引用,调拷贝构造效率太低了

	return 0;

}

1f8aa640ebbe434298646848faf2d0e8.png

类对象传值返回和传值传参会调用拷贝构造,传引用返回和传引用传参不调用拷贝构造。

来复习一下前面学的东西。

全局变量N谁都能改,没有进行封装。

C++里还是喜欢封装起来用,咱们引出static成员。

static影响生命周期

static修饰局部变量的生命周期变成全局域。

static修饰全局变量只在本源文件内可见,生命周期还是全局域。

类里面的静态成员变量,生命周期是全局的,作用域受类域限制。

static成员的特性:

1. 静态成员存在静态区,而不是类对象里面。被所有相同类的对象共享。

例:

class A
{
public:
	//构造函数
	A(int a=0)
		:_a(a)
	{
		++N;
	}

	//拷贝构造
	A(const A& aa)
		:_a(aa._a)
	{
		++N;
	}


private:
	int _a;

	static int N;
};

int main()
{
	A aa1(1);//构造
	A aa2 = 2;
}

aa1和aa2共用同一个N。

aa1有一个_a , aa2有一个_a。在初始化列表初始化自己的成员变量_a没问题。

但是不能在初始化列表初始化static成员N,因为静态成员存在静态区,而不是类对象里面。

在哪初始化静态成员?

类外面。

int A::N = 0;//定义初始化,要把类型带上,指定是A类域里的。类里面可以使用N。

定义时不加static关键字。

例:

class A
{
public:
	//构造函数
	A(int a=0)
		:_a(a)
	{
		++N;
	}

	//拷贝构造
	A(const A& aa)
		:_a(aa._a)
	{
		++N;
	}


private:
	int _a;

	static int N;
};


//生命周期是全局的,作用域受类域限制

int A::N = 0;//定义初始化,要把类型带上,指定是A类域里的。类里面可以使用N。

  ***静态成员的访问***  

1.如果公有,指定类域就能用


08c664d80c0e411e85a6f892a26638a4.png

N虽然不再aa1里面,但是算间接指定类域:

 6c5af06fd0d941fead1aae8ff03e6dee.png

 用类对象可以访问,那用指针也能访问

53310897a9344666a76e502cbed6496b.png

b521140d12194335ba2bed3e044bb082.png

用空指针去找静态成员不会报错。因为静态成员不存在类对象里面,空指针没有发生解引用。和空this指针调用成员函数一样。

以上用法的前提是静态成员是公有的。

2.私有的


1.提供一个GetN成员函数

int GetN()
	{
		return N;
	}

dea31adb09334862981ac92781aefbec.png

有对象的时候可以顺带调用。调用成员函数的时候一定要给this指针传参。

那要是没有定义对象,就是想要访问到N=0的初始值呢,就必须被迫定义一个对象,调一次构造函数,用来计数的N就+1。

2.使用静态成员函数,静态成员函数没有this指针。

也可以用对象调(或者指针)。但这里最妙的是不用对象就可以访问,指定类域就能访问私有的静态成员。

static int GetN()
	{
		return N;
	}

a94e2e6efe2844a7b0e7ca75e89fb231.png

静态成员函数的缺点是,在其函数体内不能访问类的非静态成员(包括非静态属性和方法),因为没有this指针,找不到。函数体内只能访问静态成员。

01e103b4060a4d129a9ff98ae44aa322.png

2. 静态成员函数没有this指针,只能访问静态成员。

如果有一个参数是类对象的引用,那还是能访问这个对象的成员的。

静态成员函数的意义:私有静态成员变量,不用定义类对象,通过指定类域调用静态成员函数就能访问到


练习:求1+2+3+...+n_牛客题霸_牛客网

2d22ce85a7c74c1f910e4cebafd0552a.png

class Sum
{
    public:
    Sum()
    {
        _ret+=_i;
        ++_i;
    }

    static int GetRet()
    {
        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 a[n];//初始化数组的时候调用n次Sum的构造函数
        return Sum::GetRet();
    }
};

三、友元

友元分为友元函数和友元类。

1.友元函数

函数定义在类外面,是不能访问私有成员的(日期类流插入那里用了友元)。用了友元就可以直接通过对象访问私有成员了

日期类流插入:没办法把operator<<重载成成员函数。写到类里面第一个参数默认是this指针,所以<<的左操作数一定是自定义的类。用cout就会变成 d._year<<cout。不符合我们的使用习惯和可读性。

class Date
{

	friend ostream& operator<<(ostream& out, const Date& d);//友元声明
	friend istream& operator>>(istream& in, Date& d);

    public:
    //简写,省略成员函数喽

    private:
	int _year;
	int _month;
	int _day;
}

inline ostream& operator<<(ostream& out, const Date& d)//支持的cout<<d1<<d2
{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;

	return out;
}

inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;

	return in;
}

1.友元函数可以在类外访问到类对象的私有成员,不属于成员函数

2.友元函数不能用const修饰

3.友元函数可以在类的任意位置声明,不受类访问限定符限制

4.一个函数可以是多个类的友元

5.友元函数和普通函数的调用原理相同

6.在类的内部声明,声明时要加friend

2.友元类

友元类的所有成员函数都可以访问另一个类对象的私有成员。

1.友元关系是单向的,不具有交换性。

Date是Time的友元,Date里面 通过Time对象 能访问Time的私有成员,Time里不能访问Date的私有成员。

2.友元关系不能传递

C是B的友元,B是A的友元,C不是A的友元。

3.友元关系不能继承

class Time
{
	friend class Date;//Date想偷Time家,要到Time里打声招呼。想偷谁就到谁家去打招呼(蛮那个的,嘻嘻)
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=0,int month=0,int day=0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	void SetTimeOfDate(int hour,int minute,int second)
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;

	Time _t;
};

四、内部类(了解一下)

一个类定义在另一个类的内部,这个内部的类就叫内部类。

//相当于两个独立的类
//在类外,B类的访问受A的类域和访问限定符的限制
class A
{
private:
	int _a;

public://B天生就是A的友元
	class B
	{
		int _b;
	};
};

int main()
{
	cout << sizeof(A) << endl;//结果是4.说明A类里面没有B

	A aa;

	B bb;//不能直接定义B的对象。用一个类的类型,先去局部域搜索,再去全局域搜索,不会到类域里找。
	A::B bb;
	return 0;
}

beff8714d98043af81e6a30567f59a24.png

把前面那道题改成内部类:

class Solution {
public:

    class Sum
{
    public:
    Sum()
    {
        _ret+=_i;
        ++_i;
    }

    static int GetRet()
    {
        return _ret;
    }
};

 
    int Sum_Solution(int n) 
    {

        Sum a[n];
        return _ret;
    }

    private:
    static int _i;
    static int _ret;
};
int Solution::_i=1;
int Solution::_ret=0;

五、匿名对象

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

private:
	int _a;
};

int main()
{
	//有名对象
	A aa1;
	A aa2(1);
	A aa3 = 3;//隐式类型转换
	A aa();//不能这样定义类对象,会产生歧义。把aa换成func。 A func();//带括号又不传参像个函数声明


	//匿名对象。生命周期只在当前这一行。到下一行就会调析构函数
	A();
	A(3);
	return 0;
}

class Solution
{
public:
	int Sum_Solution(int n)
	{
		//....
		return n;
	}
};

int main()
{
	Solution s;
	s.Sum_Solution(10);

	//创建对象只是为了调一个函数,还要写两行。用匿名对象一行就解决。
	Solution().Sum_Solution(10);

	return 0;
}

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

private:
	int _a;
};

A F()
{
	/*A ret(10);
	return ret;*/

	return A(10);//更方便,还会触发优化

}

六、拷贝对象时的一些优化

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

private:
	int _a;
};

void f1(A aa)
{}

void f2(A& aa)
{}

void f3(const A& aa)
{}

A f4()
{
	A aa;
	return aa;
}

A f5()
{
	return A(1);//本来应该是 构造 再拷贝构造给临时对象。临时对象再拷贝构造给ret3
}

int main()
{
	//【1】

	A aa = 1;//本来是先构造再拷贝构造,优化成直接构造A aa(1)


	//【2】传值传参

	A aa1(1);//构造
	f1(aa1);//拷贝构造
	//上面的不能优化,必须分开进行

	f1(A(1));//匿名对象 构造加拷贝构造 优化成构造


	//【3】传引用传参

	f2(1);//生成的临时变量有常性
	f2(A(1));//有的编译器认为匿名对象有常性,所以过不了

	f3(1);//引用不需要调拷贝构造。不用优化
	f3(A(1));


	//【4】传值返回

	f4();//构造加拷贝构造 没有优化

	A ret = f4();//函数里构造 返回的时候调拷贝构造给临时对象,临时对象再拷贝构造给ret。优化后:构造加拷贝构造。优化掉赋值给中间变量,返回的时候直接拷贝构造给ret

	A ret2;
	ret2 = f4();//这样就不能优化了。因为是在调赋值重载。只有构造和拷贝构造(都是构造)才有可能优化


	//【5】
	A ret3 = f5();//优化后直接取消用临时对象。也不拷贝了,直接用参数1构造ret3 

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值