C++的'大自然搬运工':一文讲透using的所有用法

0 阅读16分钟

using:C++里的“大自然搬运工”,啥都能给你搬过来!

我们刚开始写代码时总会顺手敲句 using namespace std;,结果被老司机怒怼:“头文件里别用这个!污染全局命名空间!” 然后默默改成 using std::cout;,心里嘀咕:“这俩到底有啥区别?”

后来你在类里看到这样的代码:

class Derived : public Base 
{
public:
 using Base::func; // 这又是干啥的?
 void func(int)// 自己的重载
};

再后来,你发现有人这么写:

using IntPtr = int*; // 这跟 typedef int* IntPtr; 有啥不一样?

你盯着这个 using 关键字,陷入了沉思:

“这玩意儿到底有多少种用法?怎么一会儿搬命名空间,一会儿搬类型,一会儿还能搬基类的函数?它是不是 C++ 里的‘神偷’啊?”

没错,你可以把 using 想象成一个 “大自然的搬运工”:

  • 它能从命名空间这个大仓库里,帮你把需要的“货物”(名字)搬到你面前(using std::cout)。
  • 它也能帮你把复杂的类型起个顺口的外号,比 typedef 更亲民(using IntPtr = int*)。
  • 它甚至能在继承时,偷偷把基类的成员函数“偷”到派生类里来(using Base::func),让你能在派生类里重载或使用它们。
  • 到了 C++20,这个搬运工又学了新技能——using enum,直接把枚举的成员搬到当前作用域,省得你每次写 Enum::Value 写到手指抽筋。

今天我们就来扒一扒这个“大自然搬运工”的几副面孔,看看它到底是怎么帮你搬东西的,以及什么时候该用它,什么时候该小心它搬来的东西“砸了你的脚”。

基础篇:命名空间相关的 using

这次专门聊聊命名空间相关的 using——也就是 using 声明、using 指令 和 命名空间别名。
这三兄弟在代码里天天见,但用不好就容易翻车。

一、命名空间:C++ 的“小区”

想象一下,你写了个函数叫 print,同事也写了个 print,两人都放在全局区域,编译器就懵了:“到底调用哪个?”
于是 C++ 发明了命名空间——相当于给代码划分了小区,每个小区有自己的 print,互不干扰。

namespace MyCode 
{
 void print() { std::cout << "MyCode\n"; }
}

namespace YourCode 
{
 void print() { std::cout << "YourCode\n"; }
}

但问题来了:每次调用都得写全名 MyCode::print(),手指累不累?这时候 using 三兄弟就来帮忙了。

二、命名空间别名:给长名字起个外号

有些命名空间名字长得离谱,比如 namespace very_long_and_annoying_namespace_name。 每次写都像在练打字,这时候命名空间别名就是救星:

namespace v = very_long_and_annoying_namespace_name;
v::foo(); // 舒服!

使用场景:

  • 简化标准库嵌套:namespace fs = std::filesystem; 然后爽用 fs::path。
  • 版本切换:比如你有 namespace v1 和 namespace v2,可以 namespace current = v2;,代码里全用 current::,切换版本只改一行。

小贴士:别名只在当前作用域有效,头文件里慎用,避免污染别人。

三、using 声明:精准点名,只引入需要的

using 声明就像你去食堂只打一个菜,不碰别的。语法:using 命名空间::名字;

using std::cout;
using std::endl;
cout << "hello world!" << endl; // 直接爽用,不用加 std::

好处:

  • 精准,只把用到的名字引入当前作用域。
  • 不会把整个命名空间都倒进来,避免命名冲突。

坑: 如果当前作用域已经有同名实体,会冲突。比如:

int cout = 5;
using std::cout; // 错误:cout 已经有定义了

继承中的应用(后面会说明):using Base::method; 可以改变基类成员的访问权限,也是 using 声明的一种。

四、using 指令:打开整个命名空间的大门

using 指令 就是 using namespace 命名空间;。
相当于你直接把整个小区的门打开,别人可以随意光顾(里面所有名字都暴露在当前作用域)。

using namespace std;
cout << "Hello" << endl; // 可以,但危险

危险在哪?

  • 如果当前作用域也有 cout 变量,就会冲突。
  • 如果两个命名空间都有同名函数,调用时编译器不知道选哪个,就会报二义性错误。

经典翻车案例:

namespace A { void foo(int) {} }
namespace B { void foo(double) {} }

using namespace A;
using namespace B;
foo(5); // 二义性错误!A::foo(int) 和 B::foo(double) 都能匹配?

实际上,foo(5) 会同时匹配 A::foo(int)(完美匹配)和 B::foo(double)(int 转 double),编译器无法抉择,直接罢工。

永远不要在头文件里写 using namespace! 头文件会被无数源文件包含,相当于你在所有人家里泼了一地水。源文件里写也要谨慎,最好写在局部作用域(比如函数内)。

总结

特性命名空间别名using 声明using 指令
作用给命名空间起短名引入单个名字引入整个命名空间的所有名字
语法namespace 别名 = 原名;using 命名空间::名字;using namespace 命名空间;
引入范围别名本身,不引入名字只引入指定的名字引入所有名字
冲突风险无(仅别名)低,只引入一个高,可能引起二义性
推荐使用场景长命名空间、版本切换需要频繁使用某个名字,且明确无冲突局部作用域(如函数内)简化代码

类型别名:using = typedef 的现代替代(C++11)

C++11 用 using 彻底干掉了老古董 typedef,让类型起名变得像给变量起名一样自然。

一、typedef 的“反人类”设计

在 C++98 年代,我们只能用 typedef 给类型起外号。但它的语法……怎么说呢,就像用脚设计键盘——能用,但难受。

1. 语法反直觉

typedef unsigned int uint; // 还好,像赋值反过来
typedef int* IntPtr; // 还能忍
typedef void (*Func)(intdouble)// 这啥玩意儿???

最后这个函数指针,读的时候得从中间往两边看:(*Func) 表示 Func 是一个指针,指向一个返回 void、参数是 (int, double) 的函数。正常人谁这么思考?

2. 不能直接用于模板

想给 std::vector 起个别名,比如 Vec 表示 std::vector,但 typedef 做不到模板别名。你只能曲线救国,套一层结构体:

template<typename T>
struct Vec 
{
 typedef std::vector<T> type;
};
Vec<int>::type v; // 用起来想骂娘

每次还要写 ::type,烦不烦?

3. 数组别名也难受

typedef int Array[10]; // Array 是 int[10] 的别名
Array arr; // arr 是长度为 10 的 int 数组

这种语法就像是把类型名藏在了中间,阅读性极差。

二、C++11 using 类型别名:终于像个人了!

C++11 引入了 using 关键字的新用法——类型别名。语法超级直观:

using 新名字 = 原类型;

从左到右读,和变量声明一模一样!

对比一下:

// typedef
typedef unsigned int uint;
typedef int* IntPtr;
typedef void (*Func)(intdouble);
typedef int Array[10];

// using(舒服!)
using uint = unsigned int;
using IntPtr = int*;
using Func = void(*)(intdouble);
using Array = int[10];

尤其是函数指针,using Func = void(*)(int, double); 一眼就能看出 Func 是一个类型,它代表一个函数指针,返回 void,参数是 int 和 double。清晰到爆!

三、模板别名:using 的杀手锏

这才是 using 真正的王炸!C++11 允许你直接定义模板别名,不用再套结构体了。

template<typename T>
using Vec = std::vector<T>;

Vec<int> v; // 直接当类型用,爽!

甚至可以定义更复杂的别名模板:

template<typename T>
using StringMap = std::map<std::string, T>;

StringMap<int> m; // std::map<std::string, int>

这在泛型编程中简直是神器。比如你想封装一个自定义分配器的容器:

template<typename T>
using MyVec = std::vector<T, MyAllocator<T>>;

以后代码里直接用 MyVec,简洁又统一。

四、一些小建议

  • 新代码一律用 using,别给后来人留 typedef 的坑。
  • 模板别名是你的好朋友,能少写很多 typename XXX::type。
  • 可以在头文件里使用 using 类型别名,它不引入名字,不会污染命名空间。

继承中的 using:提升基类成员

继承中的 using——一个能让你的派生类瞬间拥有“特权”的神器。解决名字隐藏、一键继承构造函数、甚至改变基类成员的访问权限,using 在继承里简直是个“权限管理大师”。

一、名字隐藏问题:为啥我调不了基类的函数?

先看个经典翻车现场:

struct Base 
{
    void func(int) { std::cout << "Base::func(int)" << std::endl; }
};

struct Derived : Base 
{
    void func(double) { std::cout << "Derived::func(double)" << std::endl; }
};

int main()
{
    Derived d;
    d.func(10); // 猜猜调用哪个?
    
    return 0;
}

输出是 Derived::func(double)!惊不惊喜?明明传的是 int,却调了 double 版本。
这就是 C++ 的名字隐藏规则:只要派生类里有一个同名函数(不管参数是否匹配),基类里所有同名函数都被隐藏了。

解决办法?用 using 声明把基类的 func 拉回来:

struct Derived : Base 
{
    using Base::func; // 引入基类所有名为 func 的函数
    void func(double) { std::cout << "Derived::func(double)" << std::endl; }
};

现在 d.func(10) 就会调用 Base::func(int),完美解决!

二、继承构造函数:再也不用手写转发构造函数了!

以前写派生类,如果基类有一堆构造函数,派生类想支持同样的构造,就得一个个手写转发:

struct Base 
{
    Base(int) {}
    Base(doubleint) {}
    // 还有几十个...
};

struct Derived : Base 
{
    Derived(int x) : Base(x) {} // 手动转发
    Derived(double d, int i) : Base(d, i) {} // 累死
    // ...
};

这简直是体力活!C++11 引入了继承构造函数,一个 using 搞定所有:

struct Derived : Base 
{
    using Base::Base; // 继承基类所有构造函数
};

就这么简单!现在 Derived 自动拥有了和 Base 一样的构造函数(除了默认、拷贝、移动构造函数外)。你可以这样用:

Derived d1(5)// 调用 Base(int)
Derived d2(3.1410)// 调用 Base(double, int)

注意事项:

  • 继承的构造函数和派生类自己定义的构造函数可以共存,编译器会根据重载决议选择。
  • 如果基类构造函数有默认参数,继承时会生成多个版本(一个带默认参数,一个不带)。
  • 不能继承构造函数的初始化列表,派生类自己的成员需要在类内初始化或通过默认值。

三、改变访问权限:让基类成员“升级”或“降级”

using 在派生类中还可以改变基类成员的访问权限。比如基类某个成员是 protected,你想在派生类中把它公开给外界:

class Base 
{
protected:
    void helper() { std::cout << "Base::helper" << std::endl; }
};

class Derived : public Base 
{
public:
    using Base::helper; // 将 protected 提升为 public
};

int main() 
{
    Derived d;
    d.helper(); // 现在可以调了!
}

同样,你也可以把基类的 public 成员在派生类中变成 private(不过一般不这么干,除非你想刻意隐藏)。

原则: using 声明引入的名字的访问权限由它在派生类中声明的位置决定。
所以放在 public: 下,它就是 public;放在 private: 下,它就是 private。

应用场景:

  • 你想暴露基类的某个 protected 成员给外部调用(比如测试代码)。
  • 你想隐藏基类的某个 public 成员(比如接口收窄)。

四、综合案例:using 在继承中的大显身手

来看一个稍微复杂点的例子:

class Base 
{
public:
    void foo(int) {}

protected:
    void bar() {}
    Base(int) {}
    Base(double) {}
};

class Derived : public Base 
{
public:
    using Base::Base; // 继承构造函数
    using Base::bar; // 把 protected 的 bar 提升为 public
    void foo(double) {} // 自己的 foo(double)

private:
    using Base::foo; // 把基类的 foo(int) 变成 private(奇怪但允许)
};

注意这里 using Base::foo 放在 private 下,会导致基类的 foo(int) 在派生类中变成 private。

但是因为我们在 public 里定义了自己的 foo(double),所以外部可以通过 Derived 对象调用 foo(double),但无法调用 foo(int)。这算是一种选择性暴露。

不过这种用法比较少见,通常我们更常用的是提升访问权限,而不是降低。

五、注意事项与最佳实践

  1. 名字隐藏时,记得 using
    只要派生类定义了同名函数(无论参数是否相同),基类同名函数就被隐藏。如果想保留,就用 using Base::func。
  2. 继承构造函数时注意成员初始化
    继承的构造函数不会初始化派生类自己的成员,这些成员需要用默认值或类内初始化。
  3. 改变访问权限要合理
    尽量不要随意降低基类成员的访问权限,这会破坏接口的一致性。提升 protected 到 public 通常是用于工具类或测试。
  4. 不要过度使用
    using 很强大,但滥用会导致代码难以理解。比如在多个基类中引入同名成员可能引发二义性。

C++17:using 声明支持逗号分隔多个名字

我们来聊一个“看起来很小,但用起来真香”的改进——C++17 允许 using 声明一次引入多个名字。以前写 using 就像挤牙膏,一次只能挤一点;现在终于可以“批量导入”了!

一、C++17 前的“牙膏式” using

在 C++17 之前,如果你想把同一个命名空间里的几个名字引入当前作用域,只能一个一个写:

using std::cout;
using std::endl;
using std::string;
using std::vector;

这代码看着就累,手指都酸了。更要命的是,如果从 std::chrono 里引入一堆时间相关的名字:

using std::chrono::duration;
using std::chrono::time_point;
using std::chrono::duration_cast;
using std::chrono::seconds;
using std::chrono::milliseconds;
// ... 还有一堆

这简直就是“重复劳动大赛”冠军!明明都是一个命名空间的东西,非要我写五六遍 using std::chrono::,手不累吗?

ps:这就像你点外卖,明明同一家店,却要一个一个菜下单,外卖小哥都得跑五趟!

二、C++17 的“批发式” using

C++17 终于听到了群众的呼声,允许在一条 using 声明中用逗号分隔多个名字:

// C++17 的写法
using std::cout, std::endl, std::string, std::vector;

爽不爽?一行搞定!
对于前面那个 std::chrono 的例子:

using std::chrono::duration, std::chrono::time_point, 
      std::chrono::duration_cast, std::chrono::seconds;

虽然还是得写 std::chrono::,但至少不用重复写 using 了。(能偷点懒也不错)

C++17 的逗号分隔 using 声明,虽然是个小改进,但写代码时能省不少手指运动。

C++20:using enum

using 专题终于聊到 C++20 了!这次要说的 using enum 简直是“枚举爱好者的福音”——以前写 enum class 觉得安全但啰嗦的朋友,这回可以解放双手了!

一、传统枚举访问的“痛”:安全与啰嗦的博弈

C++11 引入了 enum class(作用域枚举),解决了传统枚举的两个大问题:名字污染和隐式转换 。但代价是——每次访问枚举值都得写前缀,手指头都累细了:

enum class Color { Red, Green, Blue };

void printColor(Color c) 
{
    switch (c) 
    {
    case Color::Red: // 每个 case 都要写 Color::
        std::cout << "Red" << std::endl;
        break;
    case Color::Green:
        std::cout << "Green" << std::endl;
        break;
    case Color::Blue:
        std::cout << "Blue" << std::endl;
        break;
    }
}

如果枚举名字再长一点,比如 FileOpenMode、ThreadPoolPriority,那简直是“手腕终结者”。
更痛苦的是,如果在同一个函数里多次使用同一个枚举值,每次都要敲一遍前缀,代码读起来也像复读机 。

二、C++20 using enum 声明

C++20 引入了 using enum 声明,可以把枚举的所有成员“注入”到当前作用域,之后就能直接使用枚举值,不用再写前缀了:

void printColor(Color c) 
{
    using enum Color// 关键
    switch (c) 
    {
    case Red: // 直接写 Red,不用 Color::
        std::cout << "Red" << std::endl;
        break;
    case Green:
        std::cout << "Green" << std::endl;
        break;
    case Blue:
        std::cout << "Blue" << std::endl;
        break;
    }
}

是不是瞬间清爽了?这就是 using enum 的核心价值——既保留了 enum class 的类型安全,又获得了传统枚举的便捷访问。

语法形式

using enum 有两种用法:

  1. 引入整个枚举:using enum Color;
  2. 选择性引入:using Color::Red, Color::Green;(结合 C++17 的逗号分隔)
void foo() 
{
    using enum Color// 引入所有
    Color c1 = Green; // OK
    
    using Color::Red; // 只引入 Red
    Color c2 = Red; // OK
    // Color c3 = Green; // 错误,Green 没引入
}

第二种方式更精确,可以避免不必要的名字冲突。

三、应用场景

1. switch 语句中屠榜

这是 using enum 最爽的应用——switch 里枚举值多的时候,再也不用写一堆前缀了:

enum class Permission { Read, Write, Execute, Delete, Modify, ... };

void checkPerm(Permission p) 
{
    using enum Permission;
    switch (p) 
    {
    case Read: // 干净!
        // ...
        break;
    case Write:
        // ...
        break;
        // 几十个 case 都不怕
    }
}

2. 函数内频繁使用枚举

如果一个函数里多次用到同一个枚举的多个值,using enum 能省不少键盘寿命:

void processFile(FileMode mode) 
{
    using enum FileMode;

    if (mode == Read) {
        // ...
    }
    else if (mode == Write) {
        // ...
    }
    else if (mode == Append) {
        // ...
    }

}

3. 类内部暴露枚举值

在类的内部,可以通过 using enum 把枚举值直接暴露给成员函数使用,甚至可以暴露给外部:

class Widget 
{
public:
    enum class State { Active, Inactive, Pending };

    using enum State;  // 把枚举值引入类作用域

    void setState(State s) 
    {
        if (s == Active) // 直接写 Active,不用 State::
        {   
            // ...
        }
    }
};

int main() 
{
    Widget w;
    w.setState(Widget::Active); // 甚至可以直接用 Widget::Active
}

这个特性很强大——Widget::Active 直接就是枚举值,调用方甚至不需要知道它来自哪个枚举。

4. 命名空间级别暴露

我们还可以在命名空间里用 using enum,把某个枚举的值暴露到整个命名空间:

namespace MyLib 
{
    enum class LogLevel { Debug, Info, Warning, Error };

    using enum LogLevel// 将枚举值引入 MyLib 命名空间
}

void foo() 
{
    // 直接 MyLib::Debug,而不是 MyLib::LogLevel::Debug
    MyLib::LogLevel level = MyLib::Debug; 
}

这特别适合库设计——既保持了 enum class 的类型安全,又提供了简洁的访问方式。

结尾

这一路写下来算是把 C++ 里的 using 关键字从里到外扒了个遍!从命名空间的“三兄弟”到类型别名的“现代革命”,从继承里的“权限大师”到 C++17/20 的“语法糖暴击”,是不是感觉 using 这玩意比想象的能打多了?

最后的最后,我们一起来整个“using 完全指南”,把知识点串成串。

1. 命名空间相关

  • 命名空间别名:namespace fs = std::filesystem; —— 给长名字起外号,手不酸。
  • using 声明:using std::cout; —— 精准引入,只拿需要的,不污染环境(要做环保人士)。
  • using 指令:using namespace std; —— 整个大门敞开,爽但危险,头文件里千万别用!

2. 类型别名(C++11)

  • 替代 typedef:using IntPtr = int*; —— 从左到右读,符合直觉,告别“反人类”语法。
  • 模板别名:template using Vec = std::vector; —— 模板也能起别名,元编程神器。

3. 继承中的 using

  • 解决名字隐藏:using Base::func; —— 把基类被藏起来的函数拉回来。
  • 继承构造函数:using Base::Base; —— 一键继承所有构造,懒人福音。
  • 改变访问权限:放在 public 下就把基类 protected 变 public,放在 private 下就降级。

4. C++17/20 新花样

  • C++17 逗号分隔:using std::cout, std::endl, std::string; —— 一次引入多个名字,告别牙膏式 using。
  • C++20 using enum:using enum Color; —— 让 enum class 的枚举值既安全又方便。