【C++】—— 类与对象(五)

9毫米的幻想 2024-08-15 12:35:01 阅读 100

【C++】—— 类与对象(五)

1、类型转换1.1、类型转换介绍1.2、类型转换的应用1.3、explicit 关键字

2、static 静态成员2.1、static 静态成员变量2.2、static 静态成员函数2.3、总结

3、友元3.1、友元函数3.2、友元类

4.内部类5、匿名对象6、对象拷贝时的编译器优化6.1、情况一:类型转换6.2、情况二:传值传参6.3、情况三:传值返回

1、类型转换

1.1、类型转换介绍

我们知道,整型浮点型之间可以发生<code>隐式类型转换

int main()

{ -- -->

int a = 2.2;

cout << a << endl;

return 0;

}

运行结果:

在这里插入图片描述

<code>int a = 2.2;这句代码,会发生隐式类型转换。中间生成一个临时对象,将右操作数强制类型转换为左操作数的类型,再将临时对象的值赋值给

a

a

a。

那么下面代码小伙伴们见过吗

class A

{ -- -->

public :

A(int a1)

: _a1(a1)

{ }

A(int a1, int a2)

:_a1(a1)

, _a2(a2)

{ }

void Print()

{

cout << _a1 << " " << _a2 << endl;

}

private:

int _a1 = 1;

int _a2 = 2;

};

int main()

{

A aa1(5);

aa1.Print();

A aa2 = 10;

aa2.Print();

return 0;

}

运行结果:

在这里插入图片描述

诶?是不是感觉怪怪的

<code>A aa1(5);很好理解,就是调用构造函数。那A aa2 = 10;又该怎么理解呢?是拷贝构造吗?如果是拷贝构造那不应该用同类的对象来初始化吗?怎么现在是一个整型呢?

A aa2 = 10;本质是一个类型转换,10 从一个整型转换成了 A 类,其用 10 去 构造 了一个临时对象 A类,再通过拷贝构造初始化

a

a

2

aa2

aa2

在这里插入图片描述

不过编译器会对上述过程进行优化,<code>优化为直接构造。虽然语法意义上面是走上述步骤。

int main()

{ -- -->

A aa1(5);

aa1.Print();

A& aa1 = 10;

return 0;

}

这样引用行不行呢?

不行的,因为类型转换会产生一个临时对象,临时对象是常性

c

o

n

s

t

const

const 引用:const A& aa1 = 10;就可以啦

那多参数的可以吗?

可以的,C++11支持了多参数的类型转换

调用方法如下:

int main()

{

A aa1 = { 3,3 };

aa1.Print();

return 0;

}

在这里插入图片描述

1.2、类型转换的应用

<code>class Stack

{ -- -->

public:

void Push(const A& aa)

{

//···

}

private:

A _arr[10];

int _top;

};

现在,我们有一个栈,栈放着 A类 成员,怎么插入数据呢?

正常来讲是这样写

int main()

{

Stack st;

A aa1(1);

st.Push(aa1);

A aa2(2,2);

st.Push(aa2);

return 0;

}

有了类型转换后,我们就可以这样

int main()

{

Stack st;

st.Push(1);

st.Push({ 2,2});

return 0;

}

效果是一样的,但代码简洁了许多

这里 1 和 2.2 会构建一个临时对象,再将临时对象

P

u

s

h

Push

Push

在这里插入图片描述

这里,也体现了加引用尽量加

c

o

n

s

t

const

const 的重要性。因为<code>临时对象具有常性,不加

c

o

n

s

t

const

const 会造成权限放大,编译无法通过。

1.3、explicit 关键字

如果我们不想让它发生类型转换,可以增加

e

x

p

l

i

c

i

t

explicit

explicit 关键字

class A

{ -- -->

public :

explicit A(int a1)

: _a1(a1)

{ }

explicit A(int a1, int a2)

:_a1(a1)

, _a2(a2)

{ }

void Print()

{

cout << _a1 << " " << _a2 << endl;

}

private:

int _a1 = 1;

int _a2 = 2;

};

在这里插入图片描述

2、static 静态成员

2.1、static 静态成员变量

s

t

a

t

i

c

static

static 修饰的成员变量,称之为<code>静态成员变量,静态成员变量一定要在类外进行初始化静态成员变量为所有当前类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区

class A

{ -- -->

public :

A()

{

++_scount;

}

A(const A& t)

{

++_scount;

}

~A()

{

--_scount;

}

//private:

// 类⾥⾯声明

static int _scount;

// 声明时可以给缺省值吗?

//static int _scount = 0;

};

//在类外初始化

int A::_scount = 0;

int main()

{

//指定类域就可以访问_scount(公有情况下)

//因为_scount并不在某个对象中

//cout << A::_scount << endl;

cout << sizeof(A) << endl;

return 0;

}

运行结果:

在这里插入图片描述

上述代码中<code>static int _scount;就是在类中定义了一个静态成员变量

int A::_scount = 1;是 _

s

c

o

u

n

t

scount

scount 的声明,静态成员变量 一定要在类外声明

静态成员变量可以认为是全局变量,放在静态区,并不是存在类中(通过

s

i

z

e

o

f

sizeof

sizeof 也可看出),只是受类域的限制,受访问限定符限制

静态成员变量

m

a

i

n

main

main函数 之前就开辟好了,并不随对象一起开辟

可以通过直接指定类域区访问静态成员变量:类名::静态成员,因为它不是在某个对象中(公有情况下)

那在类中声明时,可以给缺省值吗?

不可以,因为声明时的缺省值是给初始化列表用的,静态成员变量不是存在类里面的,不走初始化列表。

2.2、static 静态成员函数

s

t

a

t

i

c

static

static 修饰的成员函数,称之为静态成员函数静态成员函数没有

t

h

i

s

this

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

t

h

i

s

this

this指针非静态的成员函数,可以访问任意的静态成员变量和静态成员函数

class A

{ -- -->

public :

A()

{

++_scount;

}

A(const A& t)

{

++_scount;

}

~A()

{

--_scount;

}

//静态成员函数

static int GetACount()

{

return _scount;

}

private:

// 类⾥⾯声明

static int _scount;

};

当静态成员变量是私有,就无法直接突破类域访问,这时就可以使用静态成员函数来访问。

静态成员函数没有

t

h

i

s

this

this指针,因此只能访问静态成员变量

静态成员函数同样可以通过通过 类名::静态函数 访问;当然,也可以像普通成员函数那样通过 对象.函数 来访问

int A::_scount = 1;

int main()

{

cout << A::GetACount() << endl;

A a1, a2;

A a3(a1);

cout << A::GetACount() << endl;

cout << a1.GetACount() << endl;

return 0;

}

运行结果:

在这里插入图片描述

<code>int main()

{ -- -->

cout << A::GetACount() << endl;

A a1, a2;

{

A a3(a1);

cout << A::GetACount() << endl;

}

cout << a1.GetACount() << endl;

return 0;

}

运行结果:

在这里插入图片描述

上述代码利用静态成员变量来计算该对象实例化出多少个对象。

注:在 C++ 中,任意一个 <code>{ } 中的内容都单独形成一个域

a

3

a3

a3 是在域中创建,出了作用域就销毁

同时静态成员函数只能访问静态成员变量,因为静态成员函数没有

t

h

i

s

this

this 指针

static int GetACount()

{ -- -->

_a++;

return _scount;

}

在这里插入图片描述

但是非静态成员函数可以<code>随便访问静态成员变量和静态成员函数,因为突破类域就可以访问静态成员,而非静态成员函数本来就在域中。

2.3、总结

s

t

a

t

i

c

static

static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外初始化静态成员是为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区

s

t

a

t

i

c

static

static 修饰的成员函数,称之为静态成员函数,静态成员函数没有

t

h

i

s

this

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

t

h

i

s

this

this指针非静态的成员函数,可以访问任意的静态成员变量静态成员函数突破类yu就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数静态成员也是类的成员,受

p

u

b

l

i

c

public

public、

p

r

o

t

e

c

t

e

d

protected

protected、

p

r

i

v

a

t

e

private

private 访问限定符的限制静态成员变量不能在声明位置给缺省值初始化,因为缺省值是用来给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

设已经有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

A、B、C的初始化顺序相信大家都没问题,那 D 是什么时候初始化呢?

局部的静态变量是在第一次运行到该位置才初始化

选:B

首先,对 B 和 A,后定义的先定义的先析构,B 肯定在 A 之前

因为 C 和 D 的声明周期是全局的,所以他们的析构肯定在 A 和 B 之后。再先析构 D,最后才是 C

3、友元

友元提供了一种突破访问限定符封装的方式,友元分为:友元函数友元类,在函数声明或类声明前加关键字

f

r

i

e

n

d

friend

friend,并且把友元声明放到一个类的里面

3.1、友元函数

有些情况,函数无法定义在类里面(如:不想

t

h

i

s

this

this指针放在第一个参数位置),但是函数又必须访问类中的成员变量(operator<<详情情看:)。

怎么办呢?这时 C++ 提供了友元这种突破访问限定符的方式。让类外的函数可以访问类中私有的成员变量

打个比方:小刚家有个大泳池,小明一直想去小刚家里游泳,但他们两人并不熟,这时小明肯定无法去游的。怎么办呢?小明选择和小刚交往,成为小刚的朋友,这时小明就可以去小刚家游泳啦

友元函数的基本知识:

外部友元函数可以访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数友元函数可以在类定义的任何地方声明,不受类访问限定符的限制一个函数可以是多个类的友元函数

友元声明没有明确规定放在哪个位置,但一般都是放在类的最上面

// 前置声明,都则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;

}

一个函数可以成为多个类的友元(我多交几个朋友不过分吧)

注:编译器有一个原则:我要用任何类型的变量都要向上找。上述代码要给

c

l

a

s

s

class

class B 一个前置声明,否则friend void func(const A& aa, const B& bb);

c

l

a

s

s

class

class A 不认识 const B& bb中的 B。

3.2、友元类

友元类的基本知识:

友元类中的成员函数都是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,但是 B 类不是 A 类的友元。友元关系不能传递,如果 A 是 B 的友元,B 是 C 的友元,但是 A 不是 B 的友元。

当一个类需要大量的去访问另外一个类,如果仅仅是把成员函数定义成别人的友元会很不方便,这时就可以使用友元类。

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;

}

运行结果:

在这里插入图片描述

友元类就是:我的所有成员函数都是你的友元函数

友元类是单向性的,上述代码 B 是 A 的友元,但 A 不是 B 的友元,A 是不能访问 B 中的成员变量的。如果 A 的成员函数也想访问 B 中的成员变量,可以在 B 中加入 A 的友元声明,这样就可以互相访问啦。

虽然友元提供了便利,但是友元会增加耦合度,破坏了封装,所以<code>友元不宜多用

4.内部类

我们能不能把一个类定义在另一个类的里面呢?可以的,这就是内部类

class A

{ -- -->

private :

static int _k;

int _h = 1;

public:

class B

{

public :

void foo(const A & a)

{

cout << _k << endl;

cout << a._h << endl;

}

private:

int _b;

};

};

int main()

{

cout << sizeof(A) << endl;

return 0;

}

上述代码中 B 就是 A 的内部类

那么问题来了,A 的大小是多少呢?它的大小会算上 B 类的大小吗?

我们来看下运行结果:

在这里插入图片描述

可以看到 A 的大小是<code>不计算 B 的

如果一个类定义在另一个类的内部,这个内部类就叫做内部类内部类是一个独立的类,跟定义在全局相比,他只受外部类类域限制和访问限定符限制,所以外部类定义的对象中 不包含内部类

因为内部类受到外部类的限制,所以访问内部类B需要指定类域

它也受 A 访问限定符的限制,如果 B 定义在 A 的私有,那么外面就无法访问到 B 了。

内部类默认是外部类的 友元类

也就是说默认 B 是 A 的友元,B 可以直接访问 A 的私有,但 A 无法访问 B 的私有

内部类本质也是一种封装,当 A 类和 B 类紧密关联A 类实现出来主要就是给 B 类使用,那么可以考虑把 A 类设计为 B 的内部类,如果放到

p

r

i

v

a

t

e

/

p

r

o

t

e

c

t

e

d

private/protected

private/protected 位置,那么 A 类就是 B 类的专属内部类,其他地方都用不了

5、匿名对象

之前我们实例化对象不传参是这样的

int main()

{ -- -->

A aa1;

//不能这样

//因为分不清是实例化还是函数声明

A aa2();

return 0;

}

但是 C++ 还可以这样定义对象

int main()

{

//传参

A();

//不传参

A(1)

return 0;

}

这样实例化出的是匿名对象,而我们之前定义的对象,叫做有名对象

匿名对象的声明周期只有当前这一行。一般临时定义一个对象当前用一下即可,就可以定义匿名对象

class Solution {

public:

int Sum_Solution(int n)

{

//...

return n;

}

};

int main()

{

Solution aa1;

cout << aa1.Sum_Solution(10) << endl;

cout << Solution().Sum_Solution(10) << endl;

return 0;

}

如:想调用

S

u

m

Sum

Sum

S

o

l

u

t

i

o

n

Solution

Solution 函数,但

S

u

m

Sum

Sum

S

o

l

u

t

i

o

n

Solution

Solution 是成员函数,因此正常来讲应定义一个有名对象,才能调用。但难免有些繁琐,毕竟这里定义对象仅仅是为了调用

S

u

m

Sum

Sum_

S

o

l

u

t

i

o

n

Solution

Solution 函数。这时,我们就可以使用匿名对象,这样就方便多了。

简单来理解,匿名对象没有其他的,就是为了更方便一点

但是,对匿名对象进行引用可以延长匿名对象的生命周期

int main()

{

//匿名对象是常性,用const引用

const Solution& a = Solution();

return 0;

}

6、对象拷贝时的编译器优化

现代编译器为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传参过程中可以省略的拷贝如何优化 C++ 标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更“激进”的编译还会进行跨行跨表达式的合并优化

class A

{

public :

A(int a = 0)

: _a1(a)

{

cout << "A(int a)" << endl;

}

A(const A& aa)

:_a1(aa._a1)

{

cout << "A(const A& aa)" << endl;

}

A& operator=(const A& aa)

{

cout << "A& operator=(const A& aa)" << endl;

if (this != &aa)

{

_a1 = aa._a1;

}

return* this;

}

~A()

{

cout << "~A()" << endl;

}

private:

int _a1 = 1;

};

6.1、情况一:类型转换

int main()

{

A aa1 = 1;

return 0;

}

语法上来说,上述代码会发生隐式类型转换

先用 1 构造一个临时对象 A类;再调用拷贝构造,将临时对象赋值给

a

a

1

aa1

aa1

运行结果:

在这里插入图片描述

可以看到,经过编译器的优化,实际运行结果<code>只有构造,并没有调用拷贝构造。

因为编译器认为构造一个临时对象,马上进行拷贝,中间的临时对象什么都没做,太浪费效率。因此省略了中间的临时对象,直接构造

a

a

1

aa1

aa1。

但下面这种情况就无法省略临时对象

int main()

{ -- -->

//引用的是构造的临时对象

const A& aa2 = 1;

return 0;

}

6.2、情况二:传值传参

void f1(A aa)

{ }

int main()

{

A aa1(1);

f1(aa1);

return 0;

}

我们知道,对自定义类型,传值传参要先调用其拷贝构造,那上述代码有发生优化吗?

在这里插入图片描述

没有,因为构造与拷贝构造并<code>没有发生在一个连续的步骤中,所以编译器并没有选择优化

那如果我使用匿名对象呢?

void f1(A aa)

{ -- -->}

int main()

{

f1(A(1));

return 0;

}

这样构造和拷贝构造就是在一个连续的表达式的调用里面了

运行结果:

在这里插入图片描述

按语法逻辑来说,应该是先调用<code>构造函数构造匿名对象,再传值传参调用拷贝构造

现在,经过译器的优化,直接合二为一,只剩下构造

注:调用析构函数是在出函数作用域调的,不是程序结束。虽然过程合二为一,但它还是局部变量。

int main()

{ -- -->

f1(1);

return 0;

}

在这里插入图片描述

这也是同理。

6.3、情况三:传值返回

<code>class A

{ -- -->

public :

A(int a = 0)

: _a1(a)

{

cout << "A(int a)" << endl;

}

A(const A& aa)

:_a1(aa._a1)

{

cout << "A(const A& aa)" << endl;

}

A& operator=(const A& aa)

{

cout << "A& operator=(const A& aa)" << endl;

if (this != &aa)

{

_a1 = aa._a1;

}

return* this;

}

~A()

{

cout << "~A()" << endl;

}

void Print()

{

cout << "A::Print->" << _a1 << endl;

}

private:

int _a1 = 1;

};

从语法上来说,传值返回会先将返回值拷贝到一个临时对象中,再将临时对象拷贝给函数外接收的变量

那么经过编译器优化又会是什么结果呢?

A f2()

{

A aa(1);

return aa;

}

int main()

{

f2().Print();

return 0;

}

这里我没有选择接收,而是用产生的临时对象取调用

P

r

i

n

t

Print

Print 函数

VS2019 运行结果:

在这里插入图片描述

在VS2019中:先<code>构造生成

a

a

aa

aa ,后调用拷贝构造产生临时对象。此时出函数作用域,

a

a

aa

aa 生命周期结束,调用析构函数析构

a

a

aa

aa。临时对象调用

P

r

i

n

t

Print

Print 函数后,生命周期结束(临时对象声明周期只在当前行),调用析构函数。

VS2022运行结果:

在这里插入图片描述

可以看到,VS2022的优化是比较激进的。

那他是没有生成临时对象还是没有生成

a

a

aa

aa 呢?严格来说是没有生成

a

a

aa

aa <code>只生成了临时对象。为什么呢?我们可以看到析构函数的调用是在

P

r

i

n

t

Print

Print 函数之后,此时已经出了函数作用域了。

编译器优化这么“激进”不怕出

b

u

g

bug

bug 吗?

我们重载一个 ++,再看它优不优化

//前置++重载

//A& operator++()

//{ -- -->

//++_a1;

//return *this;

//}

A f2()

{

A aa(1);

++aa;//加1,看你还优不优化

return aa;

}

int main()

{

f2().Print();

return 0;

}

在这里插入图片描述

可以看到,优化还是这么激进,可<code>结果居然是对的。

只能说确实牛。

那如果我选择接收呢?

A f2()

{ -- -->

A aa(1);

++aa;

return aa;

}

int main()

{

A ret = f2();

ret.Print();

return 0;

}

运行结果:

在这里插入图片描述

现在不仅仅是省略掉了

a

a

aa

aa,连临时对象都省略掉了,合三为一。直接将结果算好,再用 2 去构造

r

e

t

ret

ret,连 ++ 都考虑了。

不过这是 VS2022 太强了,一般的编译器走:<code>构造->拷贝构造->拷贝构造 或者 构造->拷贝构造 才是正常的。即不优化和优化一级是正常的,像 VS2022 这种优化两级的只能说变态。


好啦,本期关于类和对象的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。