【C++】《C++ Primer》第七章:类(知识点总结)

目录

7.1 定义抽象数据类型

const成员函数和this指针

返回this对象

构造函数的性质

合成的默认构造函数

默认构造函数和default

初始化列表

7.2 访问控制与封装

友元

友元的声明

声明友元不等于声明其函数(友元的作用域)

7.3 类的其它特性装

内联函数的声明

返回*this的成员函数

基于const重载函数

编程习惯!

7.4 类的作用域

定义在类外部的成员

编程习惯!

7.5 构造函数再探

构造函数初始化的顺序

关于默认构造函数

隐式的类类型转换

阻止:隐式的类类型转换

7.6 类的静态成员

静态成员

静态成员的初始化


7.1 定义抽象数据类型

const成员函数和this指针

this指针,在默认情况下,是类类型非常量版本的常量指针,例如下面的Person类的this指针默认是Person *const this

意思就是:默认情况下,不能修改this指针指向的对象是谁,但可以通过this指针修改指向对象的值。

然而,如果有一个成员变量是const类型,例如下面的const int m_a,而成员函数func要用到这个成员变量,但又不希望修改它,因此需要在函数后面加一个const

class Person
{
public:
	void func(int a) const
	{
		this->m_a = a; // 错误,因为上面声明了const,因此this指向的成员不可修改
		cout << m_a;
	}

	const int m_a = 10;
};

void test()
{
	Person p;
	p.func(20);
}

返回this对象

class Person
{
public:
	Person& add(Person p) // 返回Person引用,也对应了 return *this;
	{
		this->Age += p.Age;
		return *this;
	}
	int Age = 10;
};

void test()
{
	Person p1, p2;
	p1.Age = 10;
	p2.add(p1);
	cout << p2.Age;
}

return *this返回的就是当前这个对象,例如p2.add(p1),那么这个this指的是p2这个对象,因此第6-7行等价于:p2.Age += p1.Age; return &p2;

或者说,add()成员函数的返回值是调用add()的对象的引用,第16行是p2调用add,因此add()返回的那个*this就是p2的引用。通过输出地址,可以发现,this的地址和p2的地址是同一个。

构造函数的性质

  • 构造函数不能被声明成const型。当声明一个const对象时,需要通过构造函数向其写值(初始化过程),只有在初始化完成之后,这个对象才具有const属性。
  • 构造函数没有返回类型。

合成的默认构造函数

概念:如果我们没有显式地写一个构造函数,那么编译器会帮我们自动创建一个隐式的构造函数,就叫做“合成的默认构造函数”。

默认构造函数和default

显式补充默认构造函数,不然的话,有了其它构造函数后,就会丢失默认构造函数。

class Person
{
public:
	Person() = default; // 显式定义一个默认构造函数
	// Person() {}; // 和上面等价,但只能二选一
	Person(int age, int id, string name) : Age(age), Id(id), Name(name) {}; // 自己定义其它构造函数

	int Age = 10;
	int Id = 20;
	string Name = "wind";
};

void test()
{
	Person p1(20, 101, "wind");
	Person p2; // 调用第4行的默认构造函数
}

如果没有第4行,则第15行就出错,因为我们已经在第5行定义了一个构造函数,所以编译器就不会自动创建一个合成的默认构造函数!因此,需要自己加上一个默认构造。

初始化列表

格式如下:

class Person
{
public:
	// 初始化列表格式:
	Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};

	int Age = 10;
	int Id = 20;
	string Name = "wind";
};

7.2 访问控制与封装

友元

作用:其它类或者函数能够访问类内部私有成员。

注意:

  • 只能定义在类的内部
  • 可以定义在内部的任何位置,不论是public还是private都行,不过建议在类定义开始或结束前集中声明。
class Person
{
public:
	Person() = default;
	Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};
	friend void test(int a); // friend + 函数声明(要和函数声明一模一样)

private:
	int Age = 10;
	int Id = 20;
	string Name = "wind";
};

void test(int a)
{
	Person p1(20, 101, "wind");
	Person p2;
	cout << p2.Age; // 如果test函数不是友元,那么这句就报错,因为无法访问私有成员Age
}

友元的声明

因为友元只是制定了访问权限,并不是真正的函数声明,如果其它函数需要调用这个友元函数,那么必须在专门针对这个函数做一个声明。通常情况,把针对友元的声明和类本身放在同一个头文件中

要注意几点:

  • 如果把整个类A作为另一个类B的友元,那么类A里的所有成员都可以访问B的所有成员。
  • 把类A的成员函数作为类B的友元,那么在声明的时候,必须指出该成员函数属于那个类,例如:
class Student{public:void func();};
class Person
{
	friend void Student::func(); // 必须有Student::
}
  • 函数重载后,实际上就是2个不同的函数了,因此需要把这2个函数都声明为友元。(这也是声明友元时,必须要把形参写进去的原因)
class Student
{
public:
	void func();
	void func(int a);
};
class Person
{
	friend void Student::func(); 
	friend void Student::func(int a); // 如果不写这个,那么这个版本的func就无法访问Student的私有成员
}

声明友元不等于声明其函数(友元的作用域)

【C++】《C++ Primer》第七章:类(知识点总结)_第1张图片

7.3 类的其它特性装

内联函数的声明

  • 在类内部定义的成员函数,会被自动声明为 inline(隐式内联)
  • 为了更容易理解,建议只在类外部定义的地方说明函数是inline

即使const,也能修改变量值:mutable

关键字:mutable

作用:把一个变量变成“可变成员”

class Person
{
public:
	void func(int a) const
	{
		this->m_a = a; // 正确,即使函数是const, 但m_a是mutable的
        this->m_b = a; // 错误,因为m_b不是mutable, 因此不可被修改
		cout << m_a;
	}

	mutable int m_a = 10;
	int m_b = 10;
};

返回*this的成员函数

一大作用:可以把一系列操作串起来。

伪代码:
Person& add(int a)
{
    ...
    return *this;
}

Person& sub(int a)
{
    ...
    return *this;
}

Person p;
p.add(1).sub(2); // 串起来操作,意思是先加1,再减2

注意:针对返回的函数定义为const,那么返回的*this也会变成const对象。

基于const重载函数

直接看例子:

class Person
{
public:
	Person() = default;
	Person(int num) : Num(num) {};

	// 非const重载的display()
	Person& display()
	{
		cout << "非const: " << this->Num << endl;
		return *this; // 返回非const对象
	}
	// const重载的display()
	const Person& display() const
	{
		cout << "const: " << this->Num << endl;
		return *this; // 返回const对象
	}

	Person& add(int a)
	{
		this->Num += a;
		return *this;
	}

	int Num;
};

void test()
{
	Person p1(10); // 非const对象
	const Person p2(20); // const对象
	p1.display(); // 调用非const的display()
	p1.add(10).display(); // 调用非const的display()
	p2.display(); // 调用const的display()
}

如果没有写非const重载的display(),那么最后p1就会调用const重载的display(),对应《C++ Primer》第248页最上面:

如果让add变成constNum变成mutable,那么p1.add(10).display()就会调用constdisplay(),因为此时p1.add(10)返回的是const对象,自然会首选constdisplay()

编程习惯!

对于公用代码使用私有功能函数,就像上面为输出结果而专门写的小函数display(),原因如下:

  • 后期只需要维护这个小函数即可,就不用到处找相关代码了
  • 调试版本中可以在函数里假如调试信息,发行版就便于去掉。
  • 因为在类内定义的这些小函数,因此会自动变为inline,因此后面调用它的时候就不会带来额外的运行开销。

7.4 类的作用域

定义在类外部的成员

《C++ Primer》第243页讲到定义一个类型成员:

【C++】《C++ Primer》第七章:类(知识点总结)_第2张图片

也就意味着,这个pos的作用域就是类内部,如果在外面访问,需要加上Person::表面其作用域;同理,类内声明的函数也是如此:

class Person
{
public:
	Person() = default;
	Person(int num) : Num(num) {};
	using type1 = int; // 定义类型

	type1 func(type1 a); // 声明函数
};

Person::type1 Person::func(Person::type1 a)
{
	cout << a;
}

重点在第11行:

如果type1前不加Person::,那么会报错"未定义标识符"。

如果func前不加Person::,那么这里定义的func就不是第8行声明的那个func,而是一个新的函数,返回类型是type1,也就是int,等价于:int func(int a){cout << a;},和Person完全无关。

如果保持11行的代码,那么调用int a = p1.func(1);的时候,就会报错,因为func重定义了。

编程习惯!

即,把类似using xxx = xxx;typedef xxx yyy;放在类的开始处。

7.5 构造函数再探

构造函数初始化的顺序

成员的初始化顺序与它们在类定义中的出现顺序一致。

class Person
{
public:
	Person() = default;
	int Age;
	int Id;
	Person(int val) : Id(val), Age(Id) {}; // 错误
	Person(int val) : Age(val), Id(Age) {}; // 正确
};

void test()
{
	Person p1(10);
	cout << p1.Age << endl << p1.Id << endl;
}

Age先定义,Id后定义,因此:

8行是正确的:先用初始化Age--使用val,再初始化Id--使用Age

7行是错误的:先用初始化Age--使用Id,而此时Id还没有被初始化,因此输出p1.Id是个乱码。

注:应该尽量按顺序初始化,并且不要用某些成员去初始化其它成员(例如用Id去初始化Age)。

关于默认构造函数

如果类A没有默认构造函数,类B有一个类A的成员,那么想要定义类B的默认构造函数,就必须显式调用A的带参构造函数初始化这个类A的成员:

class Person
{
public:
	//Person() = default;
	Person(int a) {}; // 只有一个带参构造函数
	int Val = 10;
};

class C
{
public:
	Person p;
	C(int i = 0) : p(i) {}; // 写C的默认构造的时候,必须显式调用A的带参构造函数初始化成员p
};

void test()
{	
	C c;
	cout << c.p.Val;
}

如果第13行改为:C(int i),那会第18行报错,提示不存在默认构造函数。也就是说,Person没有默认构造,因此无法用(int i) : p(i)的方式去初始化p,只能用值初始化的方式。

如果第4行存在,那么Person就有默认构造函数了,从而可以省略第13行。

隐式的类类型转换

大前提:只有“具有1个实参”的构造函数能够隐式转换,没有实参、有多个实参的构造函数都不行。

例如,下面第21-22行,就是通过实参调用构造函数,然后就会把这个int型转换为Person型。实际上是通过创建一个临时Person对象来过渡的:

class Person
{
public:
	Person() = default;
	Person(int a) {};

	Person& add(const Person& p)
	{
		this->Val += p.Val;
		return *this;
	}
	int Val = 10;
};

void test()
{
	
	Person p1(10), p2;

	int a = 10;
	p2.add(a);
    p2.add(int(10));
	cout << p2.Val;
}

第20-21行等价于:
int a = 10;
Person temp(a); --> 临时对象
p2.add(temp);

阻止:隐式的类类型转换

关键词:explicit

class Person
{
public:
	Person() = default;
	explicit Person(int a) {};
	...
};

void test()
{
	
	Person p1(10), p2;

	int a = 10;
	p2.add(a); // 错误!因为第5行声明了explicit,不再允许!
	cout << p2.Val;
}

注1:explicit只允许出现在类内的构造函数声明处,类外不行!

注2:explicit只是防止隐式转换,如果要强制转换,explicit就没有用了:

class Person
{
public:
	Person() = default;
	explicit Person(int a) {};
	...
};

void test()
{
	
	Person p1(10), p2;

	int a = 10;
	p2.add(static_cast(a)); --> 对其强制转换
	cout << p2.Val;
}

7.6 类的静态成员

静态成员

关键字:static

主要为了调用方便,不需要生成对象就能调用:

class X
{
public:
    void MethodA();
    static void MethodB();
}

此时MethodB可以直接调用,X::MethodB();

MethodA必须先生成类对象才能调用,X x; x.MethodA();

注意:

  • 存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
  • 静态成员函数不与任何对象绑定在一起,因此不包含this指针。
  • 不能被声明为const
  • 因为是static,所以生命周期是持续到程序结束。
  • static关键字只能在类内部,如果在类外部定义静态成员,那么不能重复static关键字:
class Person
{
public:

	static Person& add(const Person& p)
	{
		this->Val += p.Val;  // 错误,因为声明为static后,不包含this指针
		return *this;
	}
	static void func();
	int Val = 10;
};

static void Person::func() {} // 错误,不能在类外重复关键字static
void Person::func() {} // 正确

静态成员的初始化

  • 通常情况下,不应该在类内对其进行初始化;如果要初始化,必须用constconstexpr
  • 除了静态常量成员,其它静态成员不能在类内初始化:
class Person
{
public:
	static const int a = 10; // 正确,a是个静态常量成员
	static vectorv(a); // 错误
	static vectorv; // 正确,必须在类外初始化
};
  • 静态成员可以作为默认实参,普通成员不行。
  • 静态成员可以是不完全类型(只声明,但未定义),例如:
class Person
{
public:
	static person p1; // 正确,静态数据成员可以是非完全类型
	Person *p2; // 正确,指针类型也可以是非完全类型
	Person p3; // 错误,非静态数据成员必须是完全类型
}

小结:只要想到“不和任何对象绑定在一起”,再能理解这些了。

你可能感兴趣的:(C++,c++)