结构体与对象聚合
结构体是一种对基本数据结构进行扩展,并将多个对象放置在一起来视为一个整体的类型,比如
struct Str{
int x;
int y;
};
关于结构体,我们需要注意以下几点
- 结构体的声明与定义(注意定义后面要跟分号来表示结束)
struct Str; // 声明 struct Str{ // 定义 int x; int y; };
- 仅有声明的结构体是不完整的类型(incomplete type),不完整类型可以用来定义对应的指针,但不能用来定义对应的对象
struct Str; Str * str1;
- 结构体(类)的一处定义原则:翻译单元级别
接下来我们需要了解关于数据成员(数据域)的声明与初始化
- (C++11)数据成员可以使用
decltype
来声明其类型,但不能使用auto
struct Str { // 结构体内数据成员的声明 int x; int y; }; // 结构体的定义
struct Str { decltype(3) x; // auto x = 3; 非法 int y; };
- 数据成员声明时可以引入
const
、引用等限定struct Str { const int x = 3; int y; };
- 数据成员会在构造类对象时定义
Str str1; // 此时实现了结构体内数据成员的定义
- (C++11)类内成员初始化
struct Str { int x = 3; // 注意这里仍然是声明而不是定义,并不会在此时为x分配内存并进行关联 int y; };
- 聚合初始化:从初始化列表指派初始化器
但聚合初始化的顺序与结构体内数据的定义顺序有关,这无疑会在一定程度上导致BUG的发生,所以在C++20之后引入了指派初始化器struct Str { int x; int y; }; int main() { Str m_str {3,4}; // 聚合初始化 return 0; }
struct Str { int x; int y; }; int main() { Str m_str {.x = 3,.y = 4}; // 指派初始化器 return 0; }
接下来我们来了解一下mutable
关键字
struct Str
{
mutable int x = 0; // 标识x可以被修改,可以绕过const限制,在保证常量特性的情况下修改结构体中某些数据成员的值
int y;
};
int main() {
const Str m_str ;
m_str.x = 3;
return 0;
}
在结构体中,有一类特殊的数据成员-静态数据成员,它是多个对象之间共享的数据成员。关于静态数据成员,需要关注以下几点
- 定义方式的衍化
- C++98:类外定义,
const
静态数据成员可以在类内初始化struct Str{ static int x ; }; int Str::x = 3; int main() { Str str; std::cout << str.x << std::endl; }
struct Str{ const static int x = 3; }; int main() { Str str; std::cout << str.x << std::endl; }
- C++17:内联静态成员的初始化
struct Str{ inline static int x = 3; }; int main() { Str str; std::cout << str.x << std::endl; }
- C++98:类外定义,
- 可以使用
auto
推导类型struct Str{ const static auto x = 3; }; int main() { Str str; std::cout << str.x << std::endl; }
对于静态数据成员,我们有以下几种访问方式:
.
和->
操作符:针对对象::
:针对结构体本身
最后,我们需要知道可以在类的内部声明相同类型的静态数据成员
struct Str{
static Str x;
};
Str Str::x; // 定义
int main() {
Str str;
}
成员函数(方法)
我们可以在结构体内定义函数,作为其成员的一部分:对内操作数据成员,对外提供调用接口。
struct Str{
int x = 3;
void fun(){
std::cout << x << std::endl;
}
};
在结构体中将数据和数据相关的成员函数组合在一起形成类,这是C++在C基础上引入的概念,关于类我们需要关注以下几点
- 关键字
class
class Str{ int x = 3; void fun(){ std::cout << x << std::endl; } };
类与结构体的最大区别在默认的成员访问权限上,这个在后续进行讨论
- 类本身可视为一种抽象数据类型,通过相应的接口(成员函数)进行交互
- 类本身可形成一个域,称为类域
接下来我们需要关注成员函数的声明和定义
- 类内定义(隐式内联)
class Str{ int x = 3; void fun(){ std::cout << x << std::endl; } };
- 类内声明,类外定义
class Str{ public: int x = 3; void fun(); }; void Str::fun() { }
- 类与编译期的两遍处理
class Str{ public: void fun(){ // 第一遍处理函数的声明(也就是知道了存在这么一个函数) std::cout << x << std::endl;// 第二遍处理函数的逻辑 } int x = 3; // 第一遍处理成员变量的声明 };
- 成员函数与尾随返回类型(trail returning type)
class Str{ public: using MyRes = int; MyRes fun(); int x = 3; }; //MyRes Str::fun(){ // 不合法 //return x; //} //Str::MyRes Str::fun(){ // 合法单写起来比较冗余 // return x; //} auto Str::fun() -> MyRes // 清晰明了的写法(主要逻辑是从左到右解析到Str这个域名称后会扩大对MyRes的名称的搜索域) { return x; }
之后我们关注成员函数与this
指针之间的几个知识点
this
指针是一个指向常量的指针,它是一个隐式传递的参数,用于指向当前的对象- 使用隐式的
this
指针可以构造基于const
的成员函数重载class Str{ public: void fun(){ std::cout << "Str * const this" << std::endl; } void fun() const { std::cout << "const Str * const this " << std::endl; } int x = 3; }; int main() { Str str1; const Str str2; str1.fun(); str2.fun(); // 输出结果为 // Str * const this // const Str * const this }
接下来我们来讨论成员函数的名称查找和隐藏关系,需要注意以下几点
- 函数内部(包括形参)的名称会隐藏函数外部的名称
- 类内部名称会隐藏类外部的名称
- 可以使用
this
或者域操作符来引入依赖型名称查找
除了静态数据成员外,类内也可以包含静态成员函数
class Str{
public:
static void fun(){
}
int x = 3;
};
注意,静态成员函数只能操作类的静态数据成员,因为它是属于类的一部分而不是对象的一部分。在静态成员函数中可以返回静态数据成员,一个简单的例子就是单例模式
class Str{
public:
static auto& instance(){ // 并不是完整的单例模式,只是一个演示
static Str x;
return x;
}
};
最后,成员函数也可以基于引用限定符进行重载(C++11),具体参考这里
基本用不到,用到时候再查吧
访问限定符与友元
C++中可以使用public
/private
/protected
来限定类成员的访问权限
具体的应用会在下一章详细描述
访问权限的引入使得可以对抽象数据类型进行封装,类与结构体的主要区别之一就是缺省访问权限的不同:类为private
,结构体为public
。
我们可以使用友元来打破访问权限的限制-friend
关键字,关于友元我们需要关注以下几点
- 可以声明某个类或某个函数是当前类的友元-慎用!!!
int main(); class Str{ friend int main() ; private: int x = 100; }; int main() { Str m_str; std::cout << m_str.x << std::endl; }
- 我们可以在类内首次声明友元类或者友元函数
class Str{ friend int main() ; // 注意与之前不同,在这里这句话被认为main函数的首次声明 private: int x = 100; }; int main() { Str m_str; std::cout << m_str.x << std::endl; }
注意,如果使用限定名称引入友元并非友元类(友元函数)的声明
class Str{ friend int ::main() ; // 注意这里不会被认为是main函数的首次声明,所以会报错,因为编译器找不到main函数的声明 private: int x = 100; }; int main() { Str m_str; std::cout << m_str.x << std::endl; }
- 友元函数可以在类内或者在类外定义(友元类只能在类外),对于类内定义的友元函数会被认为隐藏友元函数,通过常规名称查找无法查找到
class Str{
friend void fun(){
Str m_str;
std::cout << m_str.x << std::endl;
}
private:
int x = 100;
};
int main() {
fun(); // 无法查找到fun的定义
}
隐藏友元的好处是可以减轻编译器的负担,防止误用(友元本质上打破了类的封装,是一种不符合类的本意的行为)。我们可以通过以下方式来改变隐藏友元的缺省行为
- 在类外声明或者定义函数
- 实参依赖查找(隐藏友元的正确用法)
class Str{ friend void fun(const Str & val) { std::cout << val.x << std::endl; } private: int x = 100; }; int main() { Str m_str; fun(m_str); }