Modern C++(四)声明

4、声明

声明是将名字引入到cpp程序中,不是每条声明都声明实际的东西。定义是足以使该名字所标识的实体被使用的声明。声明包含以下几种:

  • 函数定义
  • 模板声明
  • 模板显式实例化
  • 模板显式特化
  • 命名空间定义
  • 链接说明
  • 属性声明(C++11)
  • 空声明(;)
  • 块声明(能在块中出现的声明)
    • 汇编声明
    • 类型别名声明(C++11)
    • 命名空间别名定义
    • using 声明
    • using 指令
    • using enum声明
    • static_assert
    • 不透明enum声明
    • 简单声明:引入、创建并可能会初始化一个或数个标识符的语句(典型地为变量)

说明符:声明说明符是用于描述变量、函数或者类型等声明属性的关键字或者符号。它包含以下几类:

  • 存储类说明符:像auto、register、static、extern、mutable等。
  • 类型说明符:例如int、char、float、double、void等基本类型,还有struct、union、enum、自定义类型等。
  • 类型限定符:比如const、volatile。
  • 其他说明符:typdef、inline、friend、constexpr

4.1、存储类说明符

4.1.1、存储期

存储期是对象的一种属性,定义了对象的存储的最短潜在生存期。存储期由对象的创建方式决定,一共有如下几个:

  • 静态存储期:属于命名空间作用域,或者声明static或者extern
  • 线程存储期(C++11):声明thread_local的变量,这些实体的存储在创建它们的线程持续期间存在,每个线程都有一个不同的对象。
  • 自动存储期:属于块作用域,没有static、thread_local、extern修饰,此类变量的存储持续到它们创建时所在的块退出时。
  • 动态存储期:new表达式创建的对象和隐式创建的对象

4.1.2、链接

名字可以具有外部链接、模块链接、内部链接或者无链接:

  • 无链接:无链接的标识符仅在其声明的作用域内可见,在其他作用域里无法使用。它不具备跨翻译单元引用的能力。常见的无链接标识符有局部变量、类的非静态成员等

  • 内部链接:具有内部链接的标识符在单个翻译单元(也就是单个.cpp文件及其包含的头文件)内可见,不同的翻译单元可以拥有同名的内部链接标识符,且它们彼此独立。通常使用static修饰的全局变量和函数,以及匿名命名空间中的标识符具有内部链接。

  • 外部链接:具有外部链接的标识符可以在多个翻译单元中被访问,一个翻译单元可以引用另一个翻译单元中声明的具有外部链接的标识符。普通的全局变量和函数默认具有外部链接。类以及其成员函数、静态数据成员默认具有外部链接。

类本身具有外部链接,也就是说如果在一个翻译单元定义了一个类,其他翻译单元包含头文件后可以直接使用。类的非内联成员函数(无论是否是静态函数)默认具有外部链接;内联成员函数具有内部链接:

class MyClass {
public:void inlineFunc() { /* 内联实现 */ }
};// 使用inline关键字在类定义外部定义
class MyClass {
public:void func() { ... }  // 隐式内联void inlineFunc();
};
inline void MyClass::inlineFunc() { /* 内联实现 */ }

要注意的是访问控制权限(private/protected/public)和链接属性是没有关系的。

在全局作用域中使用 static 修饰变量或函数时,会改变它们的链接属性,使其从默认的外部链接变为内部链接。这意味着这些变量或函数仅在定义它们的翻译单元(.cpp 文件)内可见,不同翻译单元可以有同名的 static 全局变量或函数,且彼此独立。

当在局部作用域(如函数内部)使用 static 修饰变量时,该变量会成为静态局部变量。静态局部变量与普通局部变量不同,它的生命周期会延长至整个程序的运行期间,而不是像普通局部变量那样在函数调用结束后就销毁。同时,静态局部变量只会在首次执行到其声明语句时进行初始化,后续函数调用时不会再次初始化。

在不同翻译单元中,可以有多个名字相同的静态变量。当使用 static 修饰全局变量时,该变量具有内部链接属性,其作用域仅限于定义它的翻译单元。因此,在不同的翻译单元中定义同名的静态变量是合法的,它们彼此相互独立,不会产生冲突。每个翻译单元中的静态变量都有自己独立的存储空间,在各自的翻译单元内可以被访问和修改,而不会影响其他翻译单元中的同名变量。

在整个程序中,不能拥有两个名称相同的具有外部链接的变量。具有外部链接的变量,其作用域可以跨越多个翻译单元。如果在不同的翻译单元中定义了两个同名的具有外部链接的变量,链接器在链接阶段会发现重复定义(违反ODR原则),从而导致链接错误。

以上这部分可以参考Modern C++(一)基本概念1.5节

  • 静态块变量:静态局部变量只会在首次执行到其声明语句时进行初始化,后续函数调用时不会再次初始化。在其后所有的调用中,声明都会被跳过。如果多个线程试图同时初始化同一静态局部变量,那么初始化严格发生一次(等价于std::call_once)。静态局部变量的内存分配发生在程序加载时(main 之前),但初始化延迟到首次执行到其声明处,静态存储期的块变量的析构函数在程序退出时调用。

4.2、语言链接

所有函数类型、所有拥有外部链接的函数名,以及所有拥有外部链接的变量名,拥有一种称作语言链接的性质。

C++支持函数重载、类成员函数等特性,为了保证这些特性能够正常工作,编译器会对函数名和变量名进行名称修饰(Name Mangling)。名称修饰会把函数的参数类型、返回类型等信息融入到修饰后的名称中,从而让链接器能够区分不同的重载函数。

“C” 语言链接:当需要在C++代码里定义能够被C语言代码调用的函数,或者要调用C语言编写的函数时,需要使用"C" 语言链接。C 语言没有函数重载等特性,所以 C 语言的函数名不会被修饰。使用 “C” 语言链接可以让 C++ 编译器按照 C 语言的规则处理函数名,避免名称修饰。

namespace A
{extern "C" int x();extern "C" int y();
}extern "C" { int x; }
  • 语言链接只能在命名空间作用域出现,意思是语言链接不能出现在除命名空间以外的作用域中
  • 语言说明的花括号不建立作用域
  • 当语言说明发生嵌套时,只有最内层的说明生效。
  • 直接包含在语言链接说明之中的声明,被处理为如同它含有 extern 说明符,用以确定所声明的名字的链接以及它是否为定义。
extern "C" int x; // 声明且非定义extern "C" { int x; } // 声明及定义
  • extern “C” 允许 C++ 程序中包含含有 C 库函数的声明的头文件,但如果与 C 程序共用相同的头文件,就必须以适当的 #ifdef 隐藏 extern “C”(C 中不允许使用),通常会用 __cplusplus:
#ifdef __cplusplus
extern "C" int foo(int, int); // C++ 编译器看到的
#else
int foo(int, int);            // C 编译器看到的
#endif

要注意的是,extern “C” 的核心作用是告诉 C++ 编译器:“对这些函数 / 变量使用 C 语言的命名和调用约定”,而不是禁止使用 C++ 特性。

以上是C++程序使用C库的方法。如果要在C程序使用C++库怎么办呢?如果希望C代码能够直接调用C++库中的函数,那么在编译C++库时,必须通过extern "C"明确标记需要暴露给C的函数,这样编译器就不会对这些函数进行名称修饰,从而让C代码能够正确链接和调用。

综上,extern "C"有两个作用

  • 当C++调用C库时:告诉C++编译器:“这个函数是C风格的,不要用C++的名称修饰去找它”
  • 当编译C++库时:告诉C++编译器:“这个函数要按C风格编译,不要进行名称修饰”。

4.3、命名空间

在命名空间块以内声明的实体会被放入一个命名空间作用域中,可以避免名字冲突。在命名空间块以外声明的实体属于全局命名空间。全局命名空间属于全局作用域,并且可以通过以 :: 开头来显式指代。多个命名空间块的名字可以相同。

  • 具名命名空间
namespace A {int a = 10;
}
  • 内联命名空间

内联命名空间内的声明将在它的外围命名空间可见。内联命名空间提供了一种方便的机制,使得我们可以在不改变现有代码调用方式的情况下,对库进行版本更新和扩展。

// 初始版本 (v1是内联的)
namespace Graphics {inline namespace v1 {class Renderer { /*...*/ };}
}// 用户代码最初是这样写的
Graphics::Renderer r; // 实际使用的是v1// 新版本 (v2成为内联的,但保留v1)
namespace Graphics {namespace v1 {  // 不再是inlineclass Renderer { /*...*/ }; }inline namespace v2 {  // 新的inline命名空间class Renderer { /*...*/ };}
}// 用户代码无需修改!
Graphics::Renderer r; // 现在自动解析为v2::Renderer
  • 匿名命名空间

匿名命名空间内部成员的作用域是具有从声明点到翻译单元结尾,具有内部链接

namespace {int a = 0;
}//int a = 10;   // 如果这个a被定义,那么匿名的a无法被使用int main() {a = 20;cout << ::a << endl;    // a = 20return 0;
}
  • 有限定查找

命名空间名可以出现在作用域解析运算符的左侧,作为有限定的名字查找的一部分。具体的名字查找过程参考Modern C++(一)基本概念 1.6、名字查找。

namespace A {int a = 10;
}A::a = 20;
  • using namespace 命名空间名

从using指令之后到指令出现的作用域结尾为止,来自命名空间名的任何名字可被无限定查找

namespace A {int a = 10;
}using namespace A;
a = 20;
  • using 命名空间名::成员名

引入命名空间中的成员,注意只能引入该命名空间直接包含的成员

#include <iostream>namespace MyLibrary {void func1() { std::cout << "MyLibrary::func1()\n"; }void func2() { std::cout << "MyLibrary::func2()\n"; }
}int main() {using MyLibrary::func1;func1();    // 可以直接调用,无需限定// func2(); // 错误:func2不可见
}

如果有嵌套类是不能直接引入的:

namespace N {class Outer {public:class Inner {};};
}// using N::Outer::Inner 错误

替代方法是:

namespace N {class Outer {public:class Inner {};};using Inner = Outer::Inner; // 在外层命名空间中定义别名
}

或者定义别名

using NestedInner = N::Outer::Inner;
  • 命名空间别名定义
#include <iostream>namespace VeryLongNamespaceName {void foo() {}
}int main() {namespace ShortName = VeryLongNamespaceName;ShortName::foo(); // 等同于VeryLongNamespaceName::foo()
}
  • 嵌套命名空间定义
// 传统方式 (C++11)
namespace A {namespace B {namespace C {void func() {}}}
}// C++17新语法
namespace A::B::C {void newFunc() {}
}

不能前向声明嵌套命名空间

// namespace Outer::Inner;  // 错误:不能单独前向声明嵌套命名空间// 正确方式
namespace Outer {namespace Inner {class A;}
}

4.4、引用声明

引用表示已存在对象或函数的别名,引用有两种:

  • S& D:S类型的左值引用
  • S&& D:S类型的右值引用

右值的定义参考Modern C++(三)表达式 3.1.2、右值。

引用有以下几点使用要注意:

  • 引用必须被初始化为指代一个有效的对象或函数
  • 引用类型不能在顶层有 cv 限定
    • 顶层 cv 限定:作用于对象本身
    • 底层 cv 限定:作用于对象所指向或引用的类型。
    • 所谓底层,就是它真实指向的内容,const修饰指针、引用类型。
    • 所谓顶层,指的是变量自身,const在变量名前面
int tmp = 10;
const int* ptr = &tmp;  // 底层
int tmp2 = 20;
ptr = &tmp2;
int* const ptr2 = &tmp; // 顶层
  • 引用不是对象;它们不必占用存储,但是编译器仍会分配一个指针大小的内存
  • 不存在引用的数组,不存在指向引用的指针,不存在引用的引用(有特殊情况,模板和typedef)
int& a[3]; // 错误
int&* p;   // 错误
int& &r;   // 错误
  • 左值引用可用于建立既存对象的别名。
  • 右值引用可用于为临时对象延长生存期
    • MyClass rref = createTempObject(); 会创建一个临时对象的副本,临时对象本身会在表达式结束后销毁。实际测试只调用了一次构造函数,这是因为编译器做了返回值优化(Return Value Optimization,RVO)
    • const MyClass& ref = createTempObject(); 延长了临时对象的生存期,但不能修改临时对象。注意,如果不加const会编译失败:非常量引用的初始化必须是左值。示例中42是右值,无法被修改,使用非常量引用可能会导致非法修改。
    int& invalid_ref = 42;  // 错误:42是右值(字面量)
    const int& valid_ref = 42;  // 合法:常量引用可绑定右值
    
    • MyClass&& rref = createTempObject(); 既延长了临时对象的生存期,又允许对临时对象进行修改。

虽然右值引用变量在声明时绑定到右值,但在后续的表达式中,它本身具有确定的存储位置,可以取地址,因此表现得像左值。

// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x);            // 调用 f(int& x)int i = 1;
f(std::move(i)); // 调用 f(int&&)

4.4.1、万能引用、完美转发与引用折叠

在模板中,T&&有两种截然不同的含义:

  • 右值引用:当T是明确类型时,如int&&,表示只能绑定右值
  • 万能引用:在模板函数中,当T是模板类型参数且使用T&&时,表示既可以绑定左值,也可以绑定右值。
void func(int& x) { std::cout << "左值" << std::endl; }
void func(int&& x) { std::cout << "右值" << std::endl; }template<typename T>
void wrapper(T&& arg) {func(std::forward<T>(arg)); // 完美转发参数到func
}int main() {int x = 10;wrapper(x);  // 转发左值,调用func(int&)wrapper(20); // 转发右值,调用func(int&&)
}

当向T&&传递左值时,T被推导为T&(左值引用),所以上例中传递x,T被推导为int&。
当向T&&传递右值时,T被推导为T(非引用类型),所以上例中传递20,T被推导为int。

要注意语法是不支持引用的引用!!但是有特殊情况。

引用折叠:通过模板或typedef中的类型操作可以构成引用的引用,此时适用引用折叠规则,右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用

typedef int&  lref;
typedef int&& rref;
int n;lref&  r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref&  r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&

上述例子中T被推导为T&后,函数参数类型为int& &&,被折叠为int&。

4.5、指针声明

指针声明包含两种:

  • 指针声明符:声明 S* D;
  • 成员指针声明符:声明 S C:😗 D; 指向 C 类中 S 类型的非静态数据成员的指针

不存在指向引用的指针和指向位域的指针。

由于存在数组到指针的隐式转换,可以以数组类型的表达式初始化指向数组首元素的指针。

int a[2];
int* p1 = a; // 指向数组 a 首元素 a[0](一个 int)的指针int b[6][3][8];
int (*p2)[3][8] = b; 

由于存在指针的派生类到基类的隐式转换,可以以派生类的地址初始化指向基类的指针,这种指针可用于进行虚函数调用。如果原指针指向某多态类型对象中的基类子对象,则可用 dynamic_cast 获得指向最终派生类型的完整对象的 void*

struct Base {};
struct Derived : Base {};Derived d;
Base* p = &d;

指向任意类型对象的指针都可以被隐式转换成指向 void 的指针,逆向的转换要求 static_cast 或显式转换,并生成它的原指针值。

int n = 1;
int* p1 = &n;
void* pv = p1;
int* p2 = static_cast<int*>(pv);
std::cout << *p2 << '\n'; // 打印 1

函数指针能以非成员函数或静态成员函数的地址初始化,由于存在函数到指针的隐式转换,取址运算符可以忽略。与函数或函数的引用不同,函数指针是对象,从而能存储于数组、被复制、被赋值等。

void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // 与 &f 相同void (*a[10])(int); // OK:函数指针数组using F = void(int); // 用来简化声明的具名类型别名
F* a[10]; // OK:函数指针数组

要注意:

  • using F = void(int); // F 是函数类型
  • using FPtr = void(*)(int); // FPtr 是函数指针类型
  • typedef void (*FuncPtr)(int); // C风格函数指针类型

4.5.1、成员指针声明

成员指针是 C++ 中一种特殊的指针类型,它指向类的成员(成员变量或成员函数),而非对象本身。成员指针包含两种:

  • 数据成员指针:指向作为类 C 的成员的非静态数据成员 m 的指针&C::m(其他形式不构成成员指针)
struct C { int m; };int main()
{int C::* p = &C::m;          // 指向类 C 的数据成员 mC c = {7};std::cout << c.*p << '\n';   // 打印 7C* cp = &c;cp->m = 10;std::cout << cp->*p << '\n'; // 打印 10
}
  • 成员函数指针:指向作为类 C 的成员的非静态成员函数 f 的指针,能准确地以表达式 &C::f 初始化
struct C
{void f(int n) { std::cout << n << '\n'; }
};int main()
{void (C::* p)(int) = &C::f; // 指向类 C 的成员函数 f 的指针C c;(c.*p)(1);                  // 打印 1C* cp = &c;(cp->*p)(2);                // 打印 2
}

成员指针有什么用?

  • 实现通用的访问逻辑:成员指针可以让你编写通用的代码来访问不同对象的相同成员,而不需要为每个对象类型编写特定的访问代码。这在处理类层次结构或模板编程时特别有用。
#include <iostream>class MyClass {
public:int value;void printValue() {std::cout << "Value: " << value << std::endl;}
};// 通用函数,使用成员指针访问对象的成员
void accessMember(MyClass& obj, int MyClass::* memberPtr) {std::cout << "Accessing member: " << obj.*memberPtr << std::endl;
}int main() {MyClass obj;obj.value = 10;// 定义成员指针int MyClass::* ptr = &MyClass::value;// 调用通用函数访问成员accessMember(obj, ptr);return 0;
}
  • 实现回调机制,允许运行时动态指定要调用的成员函数,在事件处理、状态机等场景中非常有用
#include <iostream>class EventHandler {
public:void handleEvent1() {std::cout << "Handling event 1" << std::endl;}void handleEvent2() {std::cout << "Handling event 2" << std::endl;}
};// 回调函数类型
using Callback = void (EventHandler::*)();// 事件处理器类
class EventManager {
public:void setCallback(Callback cb) {callback = cb;}void triggerEvent(EventHandler& handler) {if (callback) {(handler.*callback)();}}private:Callback callback = nullptr;
};int main() {EventHandler handler;EventManager manager;// 设置回调函数manager.setCallback(&EventHandler::handleEvent1);// 触发事件manager.triggerEvent(handler);return 0;
}

每个类型的指针都拥有一个特殊值,称为该类型的空指针值。需要将指针初始化为空或赋空值给既存指针时,可以使用值为0的整数字面量,std::nullptr_t。

4.6、数组声明

形式为T a[N];的声明为数组声明,a为由N个连续分配的T类型对象所组成的数组对象(注意,会调用N次T的构造函数,无论是否带初始化器)。

class T {
public:T(int value) : data(value) {} 
private:int data;
};// T arr[3];  // 错误:无法编译,因为 T 没有默认构造函数T arr[3] = {T(1), T(2), T(3)};  // 显式调用带参数的构造函数
T arr[3] = {1, 2, 3};  // 隐式转换(如果构造函数不是 explicit)

要注意的是,如果类没有默认构造函数,声明数组时必须提供初始化器。

T arr[3] = {1, 2, 3}; // 栈上声明数组
T *arr2 = new T[3] {1, 2, 3};   // 堆上声明数组

数组元素不能具有未知边界数组类型,所以多维数组只能在第一个维度中有未知边界。

T arr3[] = { 1, 2, 3 };
T arr3[][2] = { {1, 2}, {4, 5} };

4.7、结构化绑定

结构化绑定声明是 C++17 中一个非常实用的特性,它可以让你更方便地处理聚合类型,提高代码的可读性和简洁性。通过结构化绑定,你可以直接将聚合类型的元素绑定到命名变量上,避免了繁琐的索引或成员访问操作。

与引用类似,结构化绑定是既存对象的别名。

struct C { int x, y, z; };template <class T>
void now_i_know_my() 
{auto [a, b, c] = C(); // OK: a, b, c 分别指代 x, y, zauto [d, ...e] = C(); // OK: d 指代 x; ...e 指代 y 和 zauto [...f, g] = C(); // OK: ...f 指代 x 和 y; g 指代 zauto [h, i, j, ...k] = C(); // OK: 包 k 为空// auto [l, m, n, o, ...p] = C(); // 错误: 结构化绑定大小太小
}int a[2] = {1, 2};
auto [x, y] = a;    // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
const auto& [xr1, yr1] = a;

要注意的是被绑定的对象初始化器必须是 = 、()、 {} 之一。

4.8、枚举

枚举是一种独立的类型,它的值被限制在一个取值范围内,它可以包含数个明确命名的常量(“枚举项”)。

有以下几种形式可以声明枚举:

  • 枚举关键词 属性(可选) 枚举头名(可选) 枚举基(可选) { 枚举项列表(可选) };
  • 枚举关键词 属性(可选) 枚举头名(可选) 枚举基(可选) { 枚举项列表, };
  • 枚举关键词 属性(可选) 枚举头名 枚举基(可选);

枚举关键字:enum、enum class、enum struct
枚举头名:所声明的枚举的名字,可以省略
枚举基:冒号后面指名的某个整数类型
枚举选项列表:标识符 = 常量表达式、简单的独一无二的标识符

4.8.1、无作用域枚举

无作用域枚举也就是传统枚举,每个枚举项都成为该枚举类型的一个具名常量,在它的外围作用域可见,且可以用于要求常量的任何位置。无作用域枚举的名字可以忽略,当无作用域枚举是类成员时,它的枚举项可以通过类成员访问运算符 . 和 -> 访问。

struct X
{enum direction { left = 'l', right = 'r' };
};
X x;
X* p = &x;int a = X::direction::left; // C++11 开始才能用
int b = X::left;
int c = x.left;
int d = p->left;

无作用域枚举类型的值可以被提升或转换为整数类型,示例中给d赋值时可以直接赋值。

无作用域枚举的底层类型由编译器决定(通常是int),也可以通过枚举基来指定:

enum SmallEnum : short { A, B, C };  // 底层类型为short

若未显式指定无作用域枚举底层类型,无法前向声明(因为编译器需要知道大小)

enum E;  // 错误:必须指定底层类型
enum E : int;  // 正确

4.8.2、有作用域枚举

有作用域枚举也被称为增强型枚举,每个枚举项都成为该枚举的类型(即名字)的具名常量,它被该枚举的作用域所包含,且可用作用域解析运算符访问。没有从有作用域枚举项到整数类型的隐式转换(可以用static_cast)。

enum class Color { red, green = 20, blue };
Color r = Color::blue;switch(r)
{case Color::red  : std::cout << "红\n"; break;case Color::green: std::cout << "绿\n"; break;case Color::blue : std::cout << "蓝\n"; break;
}// int n = r; // 错误:不存在从有作用域枚举到 int 的隐式转换
int n = static_cast<int>(r); // OK, n = 21
std::cout << n << '\n'; // prints 21
int red = 10;

注意示例中的枚举项只能用作用域解析符使用,受作用域的影响,最后定义red并不会出错。

enum class/enum struct默认底层类型为int,也可以通过枚举基来指定。

  • using enum 声明:引入它所指名的枚举的枚举项名字(C++20起),可直接前向声明
enum class fruit { orange, apple };struct S
{using enum fruit; // OK:引入 orange 与 apple 到 S 中
};void f()
{S s;s.orange;  // OK:指名 fruit::orangeS::orange; // OK:指名 fruit::orange
}
  • 使用 enum:当需要与 C 代码兼容或需要隐式转换为整数时。
  • 使用 enum class/enum struct:当需要避免命名冲突、增强类型安全时(推荐优先使用)

4.9、inline说明符

inline说明符将函数声明为一个内联函数。内联函数或内联变量(C++17)有以下性质:

  • 内联函数的定义必须在访问它的翻译单元中可见,否则链接时会报错
// 翻译单元 1:main.cpp
#include <iostream>// 声明内联函数
void useInlineFunction();int main() {useInlineFunction();return 0;
}// 内联函数的定义
inline void useInlineFunction() {std::cout << "This is an inline function." << std::endl;
}

这里看到内联函数的声明并没有加inline,这是为什么呢?inline是一个定义属性,而非声明属性。它告诉编译器 “这个函数可以在调用点直接展开”,因此只需要在定义时标记。

内联实体允许多个翻译单元中有相同的定义,但每个定义必须完全一致!如果各个定义不相同是未定义行为,可能出现非预期行为,具体调用哪一个实现由编译器决定。

在内联函数中,所有函数定义中的函数局部静态对象在所有翻译单元间共享,地址相同。

// 头文件:inline_example.h
#ifndef INLINE_EXAMPLE_H
#define INLINE_EXAMPLE_H
// 内联函数定义
inline int add(int a, int b) {return a + b;
}
// 内联变量定义(C++17 起)
inline int globalValue = 10;
#endif// 翻译单元 1:file1.cpp
#include "inline_example.h"
#include <iostream>void test1() {int result = add(2, 3);std::cout << "Result in file1: " << result << std::endl;std::cout << "Global value in file1: " << globalValue << std::endl;
}// 翻译单元 2:file2.cpp
#include "inline_example.h"
#include <iostream>void test2() {int result = add(4, 5);std::cout << "Result in file2: " << result << std::endl;std::cout << "Global value in file2: " << globalValue << std::endl;
}
// 头文件:address_example.h
#ifndef ADDRESS_EXAMPLE_H
#define ADDRESS_EXAMPLE_H// 内联变量定义
inline int sharedValue = 20;#endif// 翻译单元 1:address_file1.cpp
#include "address_example.h"
#include <iostream>void printAddress1() {std::cout << "Address of sharedValue in file1: " << &sharedValue << std::endl;
}// 翻译单元 2:address_file2.cpp
#include "address_example.h"
#include <iostream>void printAddress2() {std::cout << "Address of sharedValue in file2: " << &sharedValue << std::endl;
}// 主程序:main.cpp
#include <iostream>
#include "address_example.h"void printAddress1();
void printAddress2();int main() {printAddress1();printAddress2();return 0;
}

4.10、cv(const 与 volatile)类型限定符

mutable 说明符的主要用途是让 const 成员函数能够修改类的特定数据成员。它在实现缓存机制、引用计数等场景中非常有用,但在使用时需要谨慎,确保不会破坏代码的逻辑和可维护性。

4.11、constexpr 说明符

constexpr说明符声明可以在编译时对实体求值。这些实体(给定了合适的函数实参的情况下)即可用于需要编译期常量表达式的地方。

constexpr int a = 10;
int main() {int arr[a] = {};return 0;
}

如果函数或函数模板的一个声明拥有 constexpr 说明符,那么它的所有声明都必须含有该说明符。

// 第一次声明,使用 constexpr 说明符
constexpr int add(int a, int b);// 定义函数,也必须使用 constexpr 说明符
constexpr int add(int a, int b) {return a + b;
}int main() {constexpr int result = add(2, 3);return 0;
}

当在对象声明中使用 constexpr 说明符时,意味着该对象是一个编译期常量,其值在编译时就已经确定。同时,constexpr 蕴含了 const 的语义,即该对象是只读的,不能被修改。

// 定义一个编译期可求值的函数
constexpr int square(int x) {return x * x;
}
int main() {// 使用 constexpr 声明对象constexpr int num = square(5);// 尝试修改 num,会导致编译错误//num = 10; std::cout << "Square of 5: " << num << std::endl;return 0;
}

如果square函数不加constexpr关键字,会导致编译错误。这是因为constexpr变量(如constexpr int num)必须在编译期完全确定其值,而普通函数(非 constexpr)的调用通常在运行时进行。

函数或静态数据成员(C++17 起)首个声明中的 constexpr 说明符蕴含 inline。

  • constexpr函数在首个声明中使用constexpr说明符时,隐含inline性质,这意味着编译器可以在多个翻译单元中对该函数进行内联展开,以提高程序的执行效率。同时,constexpr函数仍然保持其在编译时求值的特性,能够在常量表达式中被使用。
  • 对于类的静态数据成员,在其首个声明中使用constexpr说明符时也隐含inline。这允许在多个翻译单元中对该静态数据成员进行定义和初始化,而不会引发重复定义错误。每个翻译单元中都有该静态数据成员的一份相同的拷贝,并且在编译时就确定其值。

https://zh.cppreference.com/w/cpp/language/constexpr

4.12、decltype 说明符

检查实体的声明类型,或表达式的类型和值类别。

语法如下:

  • decltype ( 实体 )
  • decltype ( 表达式 )

decltype有几个使用注意点:

  • 如果decltype的实参是没有括号的标识表达式,或没有括号的类成员访问表达式,那么decltype产生该表达式指名的实体的类型
#include <iostream>int main() {int num = 10;// num 是未加括号的标识表达式decltype(num) anotherNum = 20;std::cout << "Type of anotherNum: " << typeid(anotherNum).name() << std::endl;return 0;
}
  • 如果实参是类型为 T 的任何其他表达式
    • 表达式值类别是亡值,则 decltype 产生 T&&;
    • 表达式 的值类别是左值,则 decltype 产生 T&
    • 表达式 的值类别是纯右值,则 decltype 产生 T

如何理解呢?

decltype 的设计目的是精确地反映表达式的类型信息,包括值类别(左值、右值等)。

  • 对于左值表达式,decltype 推导出引用类型主要基于以下几个原因:左值表示有名字、可以取地址的对象,decltype 推导出左值引用类型 T& 是为了保留左值的可修改性和可寻址性。
    int a = 20;// a 是左值表达式decltype(a) b = 30; // b 的类型是 intdecltype((a)) c = a; // (a) 也是左值表达式,c 的类型是 int&c = 40; // 修改 c 会影响 a
  • 对于纯右值表达式,纯右值通常表示临时对象或者字面量,它们没有持久的存储位置,decltype 推导出非引用类型 T 是符合其语义的。
int add(int x, int y) {return x + y;
}int main() {// add(1, 2) 是纯右值表达式decltype(add(1, 2)) result = add(1, 2); std::cout << "result: " << result << std::endl;return 0;
}
  • 对于亡值表达式,decltype 产生 T&&,亡值通常表示资源可以被移动的对象,decltype 推导出右值引用类型 T&& 是为了支持移动语义。移动语义允许在对象所有权转移时避免不必要的复制操作。
int main() {int x = 10;// 使用 std::move 将左值 x 转换为亡值decltype(std::move(x)) y = std::move(x); std::cout << "x: " << x << std::endl; // x 的值可能被移动走std::cout << "y: " << y << std::endl;return 0;
}

4.13、占位类型说明符 auto

常用情境:

  • 迭代器声明:在使用容器(如 std::vector、std::map 等)进行迭代时,迭代器的类型往往比较冗长。使用 auto 可以避免手动书写复杂的迭代器类型。
  • 复杂类型推导:当变量的类型非常复杂时,例如模板实例化类型、lambda 表达式的类型等,使用 auto 可以让代码更加简洁。
  • 范围 for 循环
  • 函数返回值类型推导(C++14 及以后)

4.14、typedef、using以及define

typedef是为现有类型创建别名(类型重命名),语法是typedef 原类型 别名。

typedef 名是既存类型的别名,而不是对新类型的声明。用 typedef 定义一个无名类或枚举时,首个 typedef 名会被视为该类型的 “官方名称”,用于链接时识别类型。

typedef unsigned long ulong;
typedef int int_t, *intp_t, (&fp)(int, ulong), arr_t[10];
typedef struct {int a; int b;} S, *pS;template<class T>
struct add_const
{typedef const T type;
};

using有两个作用:

  • 为现有类型创建别名(类型别名声明),using 别名 = 原类型
  • 引入命名空间或基类成员,参考上面4.3节
using func = void (*) (int, int);// 别名模板
template<class T>
using ptr = T*; 
// 名字 'ptr<T>' 现在是指向 T 的指针的别名
ptr<int> x;

#define(预处理指令)用于创建宏定义(文本替换),#define 标识符 替换文本。

typedef和using是编译时的类型别名,受作用域限制,只在当前作用域内有效,不会影响其他文件,被编译器视为完整类型,参与类型检查。#define是预处理阶段的文本替换,没有作用域概念,不参与类型检查,只是简单的文本替换。

4.15、属性说明符序列

属性(Attributes)是一种为程序实体(像函数、类、变量等)添加额外信息的机制,编译器可以利用这些信息来做优化或者给出特定的行为。属性的应用位置不同,其作用的对象也会不同。

语法:

  • [[属性列表]]
  • [[ using 属性命名空间 : 属性列表 ]]
  • 属性列表是由逗号分隔的零或更多个属性的序列

属性既可以在整个声明之前出现,也可以直接跟在被声明实体的名字之后,大多数其他情形中,属性应用于直接位于其之前的实体。

标准属性:

  • [[noreturn]]:指示函数不返回,通常用于那些会导致程序终止或者无限循环的函数
[[noreturn]] void fatalError(const char* message) {std::cerr << "Fatal error: " << message << std::endl;std::exit(1);
}
  • [[deprecated]]:以此属性声明的名字或实体,允许使用但因某种原因而不鼓励使用
// 使用 [[deprecated]] 标记的函数
[[deprecated("This function is deprecated, use newFunction instead.")]]
int oldFunction() {return 1;
}int newFunction() {return 2;
}int main() {// 使用被标记为 deprecated 的函数,编译器可能会发出警告int result = oldFunction();std::cout << "Result from oldFunction: " << result << std::endl;// 使用新函数result = newFunction();std::cout << "Result from newFunction: " << result << std::endl;return 0;
}
  • [[fallthrough]]:从前一 case 标号的直落是故意的,且会警告直落的编译器不应当对此诊断
  • [[maybe_unused]]:抑制对于未使用实体的编译器警告
// 使用 [[maybe_unused]] 标记的函数参数
void someFunction([[maybe_unused]] int arg) {// 函数体中没有使用 arg 参数
}int main() {[[maybe_unused]] int unusedVariable = 10;someFunction(20);return 0;
}
  • [[nodiscard]]:鼓励编译器在返回值被丢弃时发出警告
// 使用 [[nodiscard]] 标记的函数
[[nodiscard]] int calculateValue() {return 42;
}int main() {// 丢弃返回值,编译器可能会发出警告calculateValue();// 保存返回值,不会有警告int result = calculateValue();std::cout << "Result: " << result << std::endl;return 0;
}

4.16、alignas 说明符

alignas( 表达式 )
alignas( 类型标识 )
alignas( 包名 … )

alignas 说明符可用于:

  • 类的声明或定义;
  • 非位域类数据成员的声明;
  • 变量声明,但它不能应用于下列内容:
    • 函数形参;
    • catch 子句的异常形参。
struct alignas(float) struct_float
{// 定义在此
};// sse_t 类型的每个对象将对齐到 32 字节边界
struct alignas(32) sse_t
{float sse_data[4];
};

4.17、static_assert 声明

进行编译时断言检查。

static_assert( 布尔常量表达式 , 不求值字符串 )
static_assert( 布尔常量表达式 )
static_assert( 布尔常量表达式 , 常量表达式 )

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/84513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

目标检测yolo算法

yolov5s&#xff1a; 从github官网下载yolov5的算法之后&#xff0c;配置好环境&#xff08;pycharm安装包-CSDN博客&#xff09;&#xff0c;再下载权重文件&#xff0c;比如默认的yolov5s.pt&#xff1b; 运行当前文件&#xff08;detect.py&#xff09;&#xff0c;就能看…

一个超强的推理增强大模型,开源了,本地部署

大家好&#xff0c;我是 Ai 学习的老章 前几天介绍了MOE 模型先驱 Mistral 开源的代码 Agent 大模型——mistralai/Devstral-Small-2505 今天一起看看 Mistral 最新开源的推理大模型——Magistral Magistral 简介 Mistral 公司推出了首个推理模型 Magistral 及自研可扩展强…

MySQL体系架构解析(五):读懂MySQL日志文件是优化与故障排查的关键

MySQL文件 日志文件 在服务器运行过程中&#xff0c;会产生各种各样的日志&#xff0c;比如常规的查询日志&#xff0c;错误日志、二进制日志、 redo 日志和 Undo 日志等&#xff0c;日志文件记录了影响 MySQL 数据库的各种类型活动。 常见的日志文件有&#xff1a;错误日志…

湖南省网络建设与运维赛项竞赛规程及样题

湖南省职业院校技能竞赛样题 赛题说明 一、竞赛内容 “网络建设与运维”竞赛共分三个部分&#xff0c;其中&#xff1a; 第一部分&#xff1a;职业规范与素养 &#xff08; 5 分&#xff09; 第二部分&#xff1a;网络搭建及安全部署项目 &#xff08; 50 分&#xff09…

华为云Flexus+DeepSeek征文 | 基于华为云ModelArts Studio搭建AnythingLLM聊天助手

华为云FlexusDeepSeek征文 | 基于华为云ModelArts Studio搭建AnythingLLM聊天助手 引言一、ModelArts Studio平台介绍华为云ModelArts Studio简介ModelArts Studio主要特点 二、AnythingLLM介绍AnythingLLM 简介AnythingLLM主要特点AnythingLLM地址 三、安装AnythingLLM应用下载…

板凳-------Mysql cookbook学习 (十--5)

6.11 计算年龄 2025年6月11日星期三 --创建表、初始化数据 drop table if exists sibling; create table sibling (name char(20),birth date );insert into sibling (name,birth) values(Gretchen,1942-04-14); insert into sibling (name,birth) values(Wilbur,1946-11-28)…

SAP RESTFUL接口方式发布SICF实现全路径

其他相关资料帖可参考&#xff1a; https://blog.csdn.net/woniu_maggie/article/details/146210752 https://blog.csdn.net/SAPmatinal/article/details/134349125 https://blog.csdn.net/weixin_44382089/article/details/128283417 【业务场景】 外部系统不想通过RFC (需…

在windows中安装或卸载nginx

首先在nginx的安装目录下cmd查看nginx的版本&#xff1a; 在看windows的服务中是否nginx注册为服务了 如果注册了服务就先将服务卸载了 在nginx的安装目录cmd执行命令 NginxService.exe uninstall “NginxService”是对应的注册的服务名称 关闭所有的相关nginx的服务这个也…

FaceFusion 技术深度剖析:核心算法与实现机制揭秘

在 AI 换脸技术蓬勃发展的浪潮中&#xff0c;FaceFusion 凭借其出色的换脸效果和便捷的操作&#xff0c;成为众多用户的首选工具。从短视频平台上的创意恶搞视频&#xff0c;到影视制作中的特效合成&#xff0c;FaceFusion 都展现出强大的实用性。而这一切的背后&#xff0c;是…

2. Web网络基础 - 协议端口

深入解析协议端口与netstat命令&#xff1a;网络工程师的实战指南 在网络通信中&#xff0c;协议端口是服务访问的门户。本文将全面解析端口概念&#xff0c;并通过netstat命令实战演示如何监控网络连接状态。 一、协议端口核心知识解析 1. 端口号的本质与分类 端口范围类型说…

嵌入式学习笔记 - freeRTOS vTaskPlaceOnEventList()函数解析

vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait ); 函数第一个参数为消息队列等待插入链表&#xff0c; void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait ) { configASSERT( pxEventList ); /…

Ubuntu 配置使用 zsh + 插件配置 + oh-my-zsh 美化过程

Ubuntu 配置使用 zsh 插件配置 oh-my-zsh 美化过程 引言zsh 安装及基础配置oh-my-zsh 安装及美化配置oh-my-zsh 安装主题美化配置主题自定义主题 插件安装及配置官方插件查看及启用插件安装 主题文件备份.zshrcre5et_self.zsh-theme 同步发布在个人笔记Ubuntu 配置使用 zsh …

Xilinx FPGA 重构Multiboot ICAPE2和ICAPE3使用

一、FPGA Multiboot 本文主要介绍基于IPROG命令的FPGA多版本重构&#xff0c;用ICAP原语实现在线多版本切换。需要了解MultiBoot Fallback点击链接。 如下图所示&#xff0c;ICAP原语可实现flash中n1各版本的动态切换&#xff0c;在工作过程中&#xff0c;可以通过IPROG命令切…

springMVC-11 中文乱码处理

前言 本文介绍了springMVC中文乱码的解决方案&#xff0c;同时也贴出了本人遇到过的其他乱码情况&#xff0c;可以根据自身情况选择合适的解决方案。 其他-jdbc、前端、后端、jsp乱码的解决 Tomcat导致的乱码解决 自定义中文乱码过滤器 老方法&#xff0c;通过javaW…

mysql-innoDB存储引擎事务的原理

InnoDB 存储引擎支持 ACID 事务&#xff0c;其事务机制是通过 Redo Log&#xff08;重做日志&#xff09;、Undo Log&#xff08;回滚日志&#xff09; 和 事务日志系统 来实现的。下面详细解析 InnoDB 事务的工作原理。 1.事务的基本特性&#xff08;ACID&#xff09; 特性描…

在GIS 工作流中实现数据处理

通过将 ArcPy 应用于实际的 GIS 工作流&#xff0c;我们可以高效地完成数据处理任务&#xff0c;节省大量时间和精力。接下来&#xff0c;本文将结合具体案例&#xff0c;详细介绍如何运用 ArcPy 实现 GIS 数据处理的全流程。 数据读取与合并 假设我们有多个 shapefile 文件&a…

第十四届蓝桥杯_省赛B组(C).冶炼金属

题目如下: 拿到题我们来看一下&#xff0c;题目的意思&#xff0c;就是求出N个记录中的最大最小值&#xff0c;言外之意就是&#xff0c;如果超过了这个最大值不行&#xff0c;如果小于这个最小值也不行&#xff0c;所以我们得出&#xff0c;这道题是一个二分答案的题目&#x…

​​Android 如何查看CPU架构?2025年主流架构有哪些?​

在开发安卓应用或选购手机时&#xff0c;了解设备的CPU架构至关重要。不同的架构影响性能、兼容性和能效比。那么&#xff0c;​​如何查看安卓设备的CPU架构&#xff1f;2025年主流架构有哪些&#xff1f;不同架构之间有什么区别&#xff1f;​​ 本文将为你详细解答。 ​​1.…

飞算 JavaAI 2.0.0:开启老项目迭代维护新时代

在软件开发领域&#xff0c;老项目的迭代与维护一直是开发团队面临的难题。代码逻辑混乱、技术栈陈旧、开发效率低下等问题&#xff0c;让老项目改造犹如一场 “噩梦”。而飞算 JavaAI 2.0.0 版本的正式上线&#xff0c;通过三大核心能力升级&#xff0c;为老项目开发带来了全新…

Linux初步介绍

Linux是一种开源的类Unix操作系统内核&#xff0c;广泛应用于服务器、桌面、嵌入式设备等各种计算平台。它由Linus Torvalds于1991年首次开发&#xff0c;因其稳定性、安全性和灵活性&#xff0c;被全球开发者和企业广泛采用。 特点&#xff1a; 开放性&#xff08;开源&#…