C++类与对象(下)

在这里插入图片描述

取地址运算符重载

const成员函数

将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。

格式如下:

void print() const
{
    cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}

这时候会产生疑惑:

为什么要用const?const修饰的是什么?

const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由==Date const this== 变为 ==const Date const this==**

本质上是指针权限的放大和缩小的问题

来看几行代码:

// void Print(const Date* const this) 
void Print() 
{
	cout << _year << "-" << _month << "-" << _day << endl;
}
int main()
{
	const Date d2(2024, 8, 5);
	d2.Print();
	return 0;
}

这里d2会报错,引起这里产生了权限的放大。

d2.Print();
//&d2->const Date*
//这里修饰的是Date指向的内容

我们需要补充复习const的相关知识:

const int*p;//p可变,p指向的内容不变
int const*p;//p可变,p指向的内容不变
int *const p;//p不变,p指向的内容可变

const在*左边,修饰的指向的内容;在 * 右边,修饰指针本身

那么怎样的方式才会导致权限的放大?

先来给出结论:修饰指针本身的const不涉及权限的放大,缩小;但是修饰的是内容的时候,会涉及到权限的放大,缩小

对于修饰指针本身的,举个例子:

const int i=0;
int j=i;
//把i的值赋给j,j的改变不会影响i,所以说不涉及到权限的放大或者缩小

修饰的内容的话,因为const的权限相较于普通类型来说更小一些,连const修饰的内容都没有办法访问的话,那普通类型凭什么能有机会访问呢?

这时候可能会想到的解决方案是:把Date*const this前面加上const

这里插一嘴:Date*const this,在 * 右边,修饰的是指针本身,不会涉及权限的放大或者缩小

回归正题,答案是不可以,因为this指针是隐含的,它放在形参的位置,编译器会自己偷偷加入,语法有规定:this指针无论在形参还是实参的位置,都不能显示地去加上

总的来说,我们把const放在参数列表的后面,并不是修饰this指针本身,修饰得是this指针指向的内容

总结:

const修饰指向的内容和非const修饰拷贝赋值才涉及权限的放大和缩小
在这里插入图片描述
看上面这幅图,权限的平移和权限的缩小编译器不会报错,权限的放大编译器会报错

也就意味着普通对象和const修饰的对象都能被调用

那么是不是const要修饰所有的对象呢,既然好处这么多?

答案也同样的,不是啦。

被const修饰了,就需要付出一定的代价,被const修饰了之后,意味着就不能被修改了

总结:一个成员函数,不修改成员变量的建议都加上

示例代码如下:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// void Print(const Date* const this) const
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
	Date d1(2024, 9, 5);
	d1.Print();
	const Date d2(2024, 8, 5);
	d2.Print();
	return 0;
}

取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

注意点:当普通取地址运算符重载和const取地址运算符重载同时存在时,它们会去选择和自己最适配的

class Date
{
public :
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
    //return 0x12355fff,返回一个很假的地址,这是最坑人的
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};

再探构造函数

先来回忆一下:

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化 对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的 特点就完美的替代的了Init。

构造函数的特点:

  1. 函数名与类名相同。

  2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

  3. 对象实例化时系统会⾃动调⽤对应的构造函数。

  4. 构造函数可以重载。

  5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。

  6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造。

  7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始 化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始 化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表.

说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型

特点里面的6和7,我们要着重来讲。

之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅ 式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)

上面时书写格式。

注意点:
也可以这么写:

Date(int year = 1, int month = 1, int day = 1)
:_year(year+3)
,_month(month+2)
,_day(day+1)

因为有返回值,所以表达式也会有个具体的值

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地⽅。

不能出现两次,否则编译器会报错。

如:

Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
,_day(day)//这是不对的,因为,定义和函数体里面运用不一样,不能重复写多次,就像表白的时候,只有一次机会

引⽤成员变量,const成员变量,没有默认构造的类类型成员变量,必须放在初始化列表位置进⾏初始 化,否则会编译报错。

这三个变量有什么共同点呢?

引用必须得是对象定义的时候初始化的,才可以引用,否则就会是空引用或者野引用;

const成员变量,只有一次修改机会,也就是定义初始化的时候;

没有默认构造的类类型成员变量也是同理。

下面几个点建议联系在一起看:

C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的 成员使⽤的。

尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没 有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构 造会编译错误。

初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆ 关。建议声明顺序和初始化列表顺序保持⼀致。
在这里插入图片描述
结合上面这张导图会好理解很多

注意点:用缺省值去初始化,相当于是个planB,说的通俗点就是备胎

下面是示例代码:

#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
,_t(12)
,_ref(x)
,_n(1)
{
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; // 没有默认构造
int& _ref; // 引⽤
const int _n; // const
};
int main()
{
int i = 0;
Date d1(i);
d1.Print();
return 0;
}

#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date()
:_month(2)
{
cout << "Date()" << endl;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的
// 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化
int _year = 1;
int _month = 1;
int _day;
Time _t = 1;
const int _n = 1;
int* _ptr = (int*)malloc(12);//缺省值也可以是个表达式
};
int main()
{
Date d1;
d1.Print();
return 0;
}

初始化列表总结:**

⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;

⽆论是否在初始化列表显⽰初始化,每个成员变量都要⾛初始化列表初始化;

来看一道题目:

#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}

上⾯程序的运⾏结果是什么()

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出 1 随机值

E. 输出 1 2

F. 输出 2 1

分析思路:

显示在初始化列表初始化的成员变量就按照这个值初始化,所以和缺省值无关,直接不用看了,所以有2的选项全部排除,B,E,F去掉,还有个易错点:初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆ 关。建议声明顺序和初始化列表顺序保持⼀致。所以先走a2,但是a2里面的a1还没有初始化,所以是随机值,然后a2也接受的随机值,所以答案是D

放到编译器里的结果:
在这里插入图片描述

类型转换

C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数

我们学过强制类型转换,如算术类型转成另一个算术类型,可以强制类型转换的原因是都是比较数据的大小;数据强制类型转换成指针,指针就是地址,地址的本质就是编号

代码1:

// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa1
// 编译器遇到连续构造+拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();

临时对象:

编译器开辟一块空间,但是没有名字

临时对象具有常性,像是被const修饰了一样,

const A&aa2=1;//这里并不是数字1被引用,而是它的临时对象,因为临时对象具有常性,所以要加上const,使权限缩小

类似的还有:

int i=10;
double d=i;
//等价的写法
const double&rd=i;

构造函数前⾯加explicit就不再⽀持隐式类型转换。

代码2:

// 构造函数explicit就不再⽀持隐式类型转换
explicit A(int a1)

类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持.

代码3:

// C++11之后才⽀持多参数转化
A aa3 ={2,2};

使用的意义是什么呢?

举个例子:

//存储A类型的数据
class Stack
{
public:
	void Push(const A& aa)
	{}
    Stack st;
   // A aa5(5);
   // st.Push(aa5);
   // 用类型转换后的简便写法
    st.Push(5);
    //多参数也是同理
    st.Push({6,6});

对于我们之后的学习还是非常有帮助的,是个香饽饽

小小地总结就是:类型之间的转换是具有一定的关系的,内置->自定义,借助构造函数;自定义->自定义,借助构造函数

static成员

⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。

静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

// 实现⼀个类,计算程序中创建出了多少个类对象?
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
// 类⾥⾯声明
static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
    //突破类域的两种形式
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)
//cout << A::_scount << endl;
return 0;
}

⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。

⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

非静态和静态是单向的关系:非静态可以访问静态,静态不能访问非静态

突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量 和静态成员函数。

静态成员也是类的成员,受public、protected、private 访问限定符的限制。

静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员 变量不属于某个对象,不⾛构造函数初始化列表。

来做一道编程题:

求1+2+3+……+n
描述

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

数据范围: 0<n≤2000<n≤200
进阶: 空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n)

示例1

输入:
5
返回值:
15

示例2

输入:
1
返回值:
1

分析思路:

循环累加,不可以,因为不可以使用for循环,递归也不行,得写返回条件,用等差数列公式,也不行,题目要求不能使用乘除法,所以可以考虑我们这里用到的static成员函数,构造函数等知识。

代码如下:

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 arr[n];
return Sum::GetRet();
}
};

这里需要注意的是,这几行代码在OJ平台能运行起来,但是在VS等编译器平台可能会报错,因为VS不支持变长数组,在C99的执行标准才会支持变长数组,OJ平台执行标准和VS的执行标准不同,所以VS不可以

如果说想要在VS上运行出来,可以动态开辟一块空间。

代码如下:

sum*arr=new sum[n];

再来看道选择题:

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调⽤顺序为?()

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调⽤顺序为?()

A:D B A C
B:B A D C
C:C D B A
D:A B D C
E:C A B D
F:C D A B
C c;
int main()
{
A a;
B b;
static D d;
return 0}

揭晓答案是e b

构造是先走全局,全局在main函数之前被初始化,接下来的关键点是局部静态变量是第一次走到那里才算初始化,所以是e;

析构是后定义的先析构,AB先会被析构,因为CD的生命周期是全局,但是局部的静态变量会被先析构,所以是b.

友元

友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类 声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

打个比方,夏天小明想去游泳,阿伟家有游泳池,小明和阿伟聊天,成为了朋友,就能够去阿伟家游泳。

外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。

友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。

⼀个函数可以是多个类的友元函数。

友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

这里需要注意的是要加上前置声明。

#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}

但是一般不调用函数,因为会比较麻烦,类会更好点

友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
在这里插入图片描述
有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

在以后的学习中,我们会学到一句话:低耦合,高内聚

如果耦合度太高,会像多米诺骨牌一样,系统的维护成本会太高。

示例代码如下:

#include<iostream>
using namespace std;
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值