三、精通类与对象

三、精通类与对象

本文为《C++高级编程(第四版)》第八、九、十一章的部分。

  • 访问控制
  • 友元
  • 移动语义(左值引用和右值引用)
  • static和const关键字

一、访问控制

主要讲解三种访问控制权限什么时候使用。

访问说明符使用场合
public想让客户端使用的方法、访问private与protected数据成员的方法。
protected不想让客户使用的“帮助”方法。
private所有数据成员都应该是private。如果希望派生类访问,可以提供protected获取器和设置器,希望用户使用,就提供public的获取和设置器。

二、友元

本节主要介绍了实现友元的三种方式。

友元允许某个类、某个类的方法、某个全局函数,去访问另一个类的protected, private属性和方法。

2.1 友元类

将B类设为A类的友元类,这样B类中,就可以调用A类中的protectedprivate方法和成员。

class A {
    friend class B;    // 将B类设为A的友元类
private:
    void privateFun()
    {
        cout << "A的private方法" << endl;
    }
};

class B {
public:
    void bFun(A& a)
    {
        a.privateFun();
    }
};

int main()
{
    A a;
    B b;
    b.bFun(a);
    return 0;
}

// 结果
// ======
// A的private方法

原本void privateFun()是A的private方法,只能在类A内部使用。将B类作为A的友元类之后,B类中就可以使用A的对象方法私有方法了。

2.2 友元成员方法

有时,只想一个类的特定方法去访问另一个类的保护和私有成员,这时就可以只将一个方法友元。

2.2.1 前置声明

这里需要前置声明的知识才可继续,如果您已了解,可跳过此节。

下面这段代码是一定编译不过的,因为第一行中,还没有定义b就开始使用了。

int a = b;
int b = 10;

类也是一样。

// class B;    1. 前置声明
class A {
private:
    B* b;
};

class B {
    
};

这样也是编译无法通过的,因为编译器找不到B这个类。不知道这个存不存在。

因为编译器不知道这个类存不存在,所以无法为这个指针分配空间。这个时候就需要一个方法告诉编译器,这个类的存在的,指针大小固定的,你大胆的分配空间即可。

告诉编译器这个类存在的方式就是前置声明,如注释1。

前置声明只能告诉编译器,这个类是存在的,并不能告诉编译器这个类需要分配多大的空间。

上个例子中,由于创建的是B的指针,所以编译器知道,4或8个字节。如果创建B的对象,前置声明将不起作用,编译器还是无法分配空间。

2.2.2 写友元成员方法

写友元成员方法的时候要注意了,其有很多细节需要注意的点。

class A;    // 1. 首先前置声明A

class B {   // 2. 先定义B类
public:
    void bFun(A& a);   // 3. 只声明不实现
};

class A {
private:
    void privateFun()
    {
        cout << "A的私有方法" << endl;
    }
};


void B::bFun(A& a)  // 4. 实现B的方法
{
    a.privateFun();
}

int main()
{
    A a;
    B b;
    b.bFun(a);
    
    return 0;
}

// 结果
// ======
// A的private方法

注释1:B的函数形参中,有A的引用。在底层汇编角度,引用类型就是封装好的指针,所以只要告诉A在这个类是存在即可,所以需要对A前向声明。

注释2:为什么不能先定义类A呢?这样就不需要前置声明类A。因为编译器需要知道B类中是否存在void bFun()这个函数,此时前置声明类B是无法达到要求的,所以必须先实现类B。

注释3:为什么不能立刻实现,而是需要在类A实现之后才实现呢?仔细看这个函数内部,其使用了类A的对象,编译器还是无法得知类A之中的具体细节。所以还是会报错。

当编译器不知道类A的具体细节,就进行使用时,会报不允许使用不完整类型的错误。

2.3 全局函数友元

全局函数也可以做为友元去访问类的保护和私有成员。

class A {
    friend void myFun();   
private:
    void privateFun()
    {
        cout << "A的private方法" << endl;
    }
};

void myFun(A& a)
{
    a.privateFun();
}

int main()
{
    A a;
    myFun(a);
    
    return 0;
}

注意哦,全局函数的实现一定要在类实现的后面哦,否则调用对象a会报使用不完整类型的错误哦。

友元是一种违反封装原则的方式,并且很容易被滥用,所以只在特定情况下才使用。

三、移动语义

本节着重介绍了C++中的移动语义。

移动语义主要解决对象所有权变更的问题,是C++11的新特性,也是重要的特性之一,为什么需要这个特性呢?

以往C++要想将对象a的所有权,需要先将a对象赋值一份给b对象,再将a对象删除。这其中就涉及到对象的拷贝复制和析构,这样开销就比较大,而移动语义就是解决此问题。

移动语义就是将一个对象的所有权变更给另一个对象,变更之后原来的对象会处于有效,但不确定的状态,通常会为空。

为了实现移动语义,首先要明确那些资源可以被移动,在大多数情况下,右值都是可以被安全的移动的,那么什么是右值呢?

3.1 右值和右值引用

3.1.1 左值和右值

和右值对应的叫做左值,往往在等号左边的被称为左值,而右值往往在等号的右边,一种比较广泛认同的说法为:

  • 可以取地址,有名字,非临时的就是左值。

  • 不能取地址,没有名字,临时的就是右值。

int a = 4 * 2;

在这个例子中,a就是左值,而4 * 2就是右值。

所以可以得出以下结论:

非匿名对象、变量、函数返回的引用,const对象等都是左值。立即数,函数返回的值等都是右值。

有没有办法将左值变为右值呢?有的,调用std::move()函数。

int a = 10;
int&& b = move(a);

为什么需要将左值变为右值呢?接下来左值和右值引用中将有作用。

3.1.2 左值引用

左值引用就是我们一直经常使用的引用,必须声明时就进行初始化,并且不可重新指定。

int a = 10;
int& b = a;

在底层汇编角度,左值引用其实就是指针,其等号右边的内容必须可以取地址(即左值),像下面这种则不可以(10为右值),因为10是存放在汇编语句中,是一个立即数,根本就没有地址。

int& a = 10;

但是使用常引用则可以指向数字10。

const int& a = 10;

因为这句话编译器其实进行了额外的操作,这句话就相当于这样:

const int temp = 10;
const int& a = temp;

也就是说a其实是这个临时变量的引用。并且这样引用之后,只能进行读操作,无法进行修改。

那有没有办法可以引用常量,而且可以修改呢?右值引用。

3.1.3 右值引用

右值引用其实就是一个指向临时对象的引用,其只能指向临时对象(即右值),比如上面例子中的10,否则编译器会报错,例如。

int&& a = 10;
++a;   
cout << a << endl;   // 最后a的结果为11

这种方式指向一个右值就可以修改。

右值引用最大的优点并不是可以以引用的方式修改一个右值,其最大的优点可以移动语义,就是本文开头所提到的更改对象所有权,详细内容在下节讲解。

3.2 移动语义

移动语义由右值引用实现,若要对自定义的类使用移动语义,需要实现移动构造函数和移动运算符。

A(A&& a);              // 移动构造函数
A& operator=(A&& a);   // 移动运算符

下面举例说明:

#include <iostream>
#include <string>

using namespace std;

class A {

public:
	A()
	{
		cout << "构造函数" << endl;
		m_string = new string("初始");
		m_num = 1;
	}

	~A()
	{
		cout << "析构函数" << endl;

		// 这里要注意了,有可能string拥有权已经没了
		if (m_string == nullptr)
			delete m_string;
	}

	A(A& a)
	{
		cout << "拷贝构造函数" << endl;
		m_string = a.m_string;   // 调用string的拷贝构造函数
	}

	A& operator=(A& a)
	{
		cout << "拷贝赋值运算符" << endl;
		m_string = a.m_string;
		return *this;
	}

	A(A&& a) noexcept
	{
		cout << "移动构造函数" << endl;
		m_string = move(a.m_string);    // 这里使用move将string对象所有权变更

		m_num = a.m_num;    // 这种复制起来开销不大的属性,普通的复制然后清零就可以了,当然也可以用move
		a.m_num = 0;
	}

	A& operator=(A&& a) noexcept
	{
		cout << "移动赋值运算符" << endl;
		m_string = move(a.m_string);    // 这里也是一样
		m_num = a.m_num;    
		a.m_num = 0;

		return *this;
	}

	string* m_string;
	int m_num;

};


int main()
{
	A a;
	A b;

	a = b;    // 这里用的是拷贝赋值运算符
	a = move(b);    // 这里才用移动赋值运算符

	return 0;
}

// 结果
// =============
// 构造函数
// 构造函数
// 拷贝赋值运算符
// 移动赋值运算符
// 析构函数
// 析构函数

四、static关键字

4.1 静态方法

静态方法不属于任何一个对象,所以其中是没有this指针的。当一个对象调用静态方法时,静态方法不会访问这个对象的非静态数据成员。

静态方法可以访问protected, private静态数据类型。如果向这个静态方法中传递了对象的引用或指针,则也可以访问其protected, private非静态数据类型。

4.2 静态成员

并不是所有对象都需要包含某个属性的副本,可以将某个属性设为静态成员,供所有对象使用。

要使用静态成员首先要在类中声明静态成员。

class A
{
public:
    static int m_snum;
};

然后在使用此静态成员的文件中,像定义全局变量一样再定义一次静态成员。

static int A::m_snum = 100;
inline变量 c++17

上文中使用的静态成员要在两个地方写,有些麻烦,内联变量可以不用在源文件中分配空间,即不需要在使用此静态成员的文件中,定义了。

class A
{
public:
    static inline int m_snum = 100;   // 可直接在此定义
};

4.3 静态链接

C++编译一个程序通常有四个步骤:预处理、汇编、编译、链接。其中前三步都是每个文件单独执行的,最后才会各个文件彼此链接。其中链接又被分为外部链接和内部链接(静态链接)。

外部链接就是该变量、函数等名称,可以被其它文件访问。

内部链接则意味着此名称在其它文件中无效。

例如:

// a.cpp
void f();   // 只有声明,定义在b.cpp中,这个函数现在就是外部链接

int main()
{
    f();
    return 0;
}

// b.cpp
void f()
{
    cout << "f()" << endl;
}

// 结果
// ==========
// f()

如果将a.cpp中的void f();修改为static void f();将会报错,因为这样就成了内部链接,其它文件将无法访问此函数。

五、const常量

5.1 const方法

常量是无法被改变的,所以当使用常量的对象时,编译器不允许调用方法,除非这些方法承诺不会修改对象的数据。这种承诺不会修改常量数据的方法就是常量方法。

void fun() const;

这个函数实际上就是给this指针上了个const,只要修改其中任意变量,都会报错。修改函数参数是不影响的。

mutable数据成员

有时候想用常量方法,但是又想修改对象中部分属性。只需要将此属性声明为mutable类型即可。

class A
{
public:
    mutable int a;   // 在常量方法中就可以修改了
};

5.2 const 方法重载

在C++中,仅返回值不同是可不重载的,必须参数类型或个数不同。但是返回值是不是const是可以作为重载条件的。通常情况下,const版本和非const版本实现是一样的,为了避免代码重复可以使用const_cast()

5.3 constexpr 关键字

C++很多时候需要的是常量表达式,例如在定义数组的长度的时候。

常量表达式是一种不会改变,并且在编译的过程就可以得到结果。

下面代码在C++中是无效的,因为函数返回的是常量,而不是常量表达式。

注:下列代码在g++ c++17的环境下是可以执行,并得到正确的结果。其原因是g++此编译器进行了扩展,而不是C++标准语法,故不建议使用。

const int getSize()   // constexpr int getSize()
{
    return 30;
}

int main()
{
    int arr[getSize()];
    return 0;
}

constexpr可以使函数返回的是常量表达式。换成注释中的内容即可正确运行。

constexpr修饰的函数也有一些限制,其确保在编译时期就可以得到值。一切需要运行时候才能确定值的语法,均不可在constexpr中出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值