C++中大括号的用法合集

前言

花括号在 C++ 里是复用最严重的符号:它既是语句块的分界,又是初始化语法,还承担聚合初始化、成员初始化列表、lambda 函数体、命名空间与枚举的定义边界。C++11 引入「列表初始化」(list-initialization,也常被叫作统一初始化 uniform initialization)之后,几乎所有能用括号初始化的地方都可以换成花括号,但两者的语义并不等价------这正是大量面试题和线上事故的来源。

最典型的误解是「花括号只是圆括号的另一种写法」。实际上 std::vector<int> v(10) 和 std::vector<int> v{10} 得到的东西完全不同:前者是 10 个值为 0 的元素,后者是 1 个值为 10 的元素。另一类误解是「花括号初始化一定更安全」------它在窄化转换上确实更严格,但在构造函数重载决议上会优先选择 std::initializer_list 版本,反而可能选错。

本文把花括号的用法按语境分成几类:初始化、类型定义、作用域与生命周期、以及语法消歧,逐类说明规则和边界。示例以 C++17 为基准,涉及 C++20 的指定初始化器会单独标注。

一、列表初始化:统一的外壳,不同的内核

1.1 基本形式

列表初始化指用 {...} 初始化对象,分直接列表初始化和拷贝列表初始化:

cpp 复制代码
// 适用于 C++17

int a{42};                 // 直接列表初始化

int b = {42};              // 拷贝列表初始化

int c{};                   // 值初始化,c 为 0

double d = {};             // d 为 0.0

int arr[3]{1, 2, 3};

std::string s{'x'};        // 注意:这是 initializer_list<char> 版本

空花括号 {} 表示值初始化 (value-initialization):标量类型清零,类类型调用默认构造函数。写 int x{}; 比 int x = 0; 更能表达「我就是要一个零值」的意图,也避免了漏写初始化。

1.2 窄化转换是编译期错误

列表初始化最实际的价值是拒绝窄化(narrowing conversion):

cpp 复制代码
// 适用于 C++17

int x1 = 3.9;      // 合法,x1 为 3,编译器可能只给警告

int x2{3.9};       // 错误:浮点转整数属于窄化

char c1 = 300;     // 合法(实现定义/截断)

char c2{300};      // 错误:常量 300 超出 char 范围

int y1 = 70000;    // 可能溢出,取决于实现

short y2{70000};   // 错误:窄化

窄化的定义包括:浮点转整数(即使常量值恰好是整数也不行)、高精度整数转低精度整数(除非是常量表达式且值能原样表示)、整数转浮点(除非是常量且值精确可表示)、以及 long double 转 double/float。这条规则把一批运行期才暴露的截断错误提前到了编译期。

需要注意:int x2{3.9} 报错不是所有编译器都给出同样的措辞,但标准要求这是 ill-formed,编译器必须诊断。

1.3 initializer_list 构造函数优先

当一个类同时有 initializer_list 构造函数和其他构造函数时,只要花括号里的元素能转成 initializer_list 的元素类型,编译器就优先选它,哪怕别的构造函数匹配得更好:

cpp 复制代码
// 适用于 C++17

#include <vector>



std::vector<int> v1(10);      // 10 个元素,每个为 0

std::vector<int> v2{10};      // 1 个元素,值为 10

std::vector<int> v3(10, 1);   // 10 个元素,每个为 1

std::vector<int> v4{10, 1};   // 2 个元素:10 和 1

v2 和 v4 的行为经常被误认为「编译器 bug」,其实是标准规定的重载决议顺序。要强制走普通构造函数,只能用圆括号(或 std::vector<int>(10) 再移动)。

同样的陷阱出现在 std::string:

cpp 复制代码
// 适用于 C++17

std::string s1(3, 'a');   // "aaa"

std::string s2{3, 'a'};   // 错误:元素 3 是 int,转 char 属于窄化

s2 会先去匹配 std::initializer_list<char> 构造函数,而 3 从 int 转 char 是窄化转换,标准要求编译期报错。想要重复字符只能用圆括号 (3, 'a'),或者写成 {'a', 'a', 'a'}。

二、花括号在类型定义中的位置

2.1 聚合初始化

聚合类型(aggregate)没有用户提供的构造函数、没有私有或受保护的非静态数据成员、没有虚函数。C++17 起允许有 public 基类。聚合可以用花括号按成员声明顺序初始化,未列出的成员被值初始化:

cpp 复制代码
// 适用于 C++17

struct Point {

    int x;

    int y;

};



struct Line {

    Point from;

    Point to;

    int color;

};



Point p{1, 2};

Line l{{0, 0}, {10, 10}, 0xFF0000};

Line partial{{0, 0}};        // to 为 {0,0},color 为 0

嵌套的花括号不能省错位置。Line l{0, 0, 10, 10, 0xFF0000} 也能编译(花括号省略,brace elision),可读性差且一旦结构改动就悄悄错位,不建议。

C++20 才支持指定初始化器(designated initializers):

cpp 复制代码
// 需要 C++20

Point p{.x = 1, .y = 2};

它要求按声明顺序书写,不能跳过再回头,也不能与普通位置初始化混用。C++17 及以前没有这个语法,MSVC 在 C 模式下有过类似扩展,但不是标准 C++。

2.2 非静态数据成员默认初始化

类内可以直接给成员写默认值,语法同样用花括号:

cpp 复制代码
// 适用于 C++17

class Config {

public:

    int retries{3};

    std::string host{"localhost"};

    std::vector<int> ports{8080, 8081};

};

这种写法叫 NSDMI(non-static data member initializer)。它会改变类是否为聚合类型的判定:C++11 里带 NSDMI 的类不是聚合,C++14 起放宽了这个限制,带 NSDMI 也可以是聚合。写库代码时要留意这对聚合初始化的影响。

2.3 构造函数初始化列表

成员初始化列表用的是冒号加圆括号或花括号:

cpp 复制代码
// 适用于 C++17

class Widget {

public:

    Widget(int id, std::string name)

        : id_{id}, name_{std::move(name)}, tags_{} {}



private:

    int id_;

    std::string name_;

    std::vector<std::string> tags_;

};

两个必须记住的点:

  1. 初始化顺序由成员声明顺序决定,不是列表里的书写顺序 。上例中如果 name_ 声明在 id_ 前面,而初始化表达式里用了 id_,读到的就是未初始化的值。
  2. 用花括号初始化成员时同样会优先选 initializer_list 构造函数(如 tags_{} 是空列表,安全)。

三、作用域、生命周期与 RAII

3.1 裸花括号就是作用域

没有关键字引导的 {} 会引入一个块作用域,块内变量在右花括号处逆序析构:

cpp 复制代码
// 适用于 C++17

#include <mutex>



std::mutex g_mutex;

int g_value = 0;



void update() {

    {

        std::lock_guard<std::mutex> lock(g_mutex);

        ++g_value;

    }   // 锁在这里释放,而不是函数结束时

    // 后续不需要锁的工作

}

这是 C++ 里最常用的「缩窄临界区」手法。等价写法是 if (true) 包一层,但裸块更清楚。同理可以用于限制大对象的生命周期,让它尽早释放。

3.2 if / for / while 的块

if (cond) { ... } 的花括号本身没有额外语义,但在宏定义里必须写,否则会出现悬垂 else:

cpp 复制代码
// 适用于 C++17

#define SAFE_DELETE(p) do { delete (p); (p) = nullptr; } while (0)

宏体用 do { ... } while (0) 包起来是标准做法:它让宏在语法上等价于一条语句,调用处加分号不会破坏 if/else 结构。

3.3 lambda 与函数体

lambda 的捕获列表用方括号,函数体用花括号:

cpp 复制代码
// 适用于 C++17

auto add = [](int a, int b) { return a + b; };

auto counter = [n = 0]() mutable { return ++n; };

捕获列表为空时,lambda 可以隐式转换为函数指针,这也意味着它不能捕获任何东西。C++14 起支持初始化捕获(n = 0),C++20 起支持模板形参的泛型 lambda。

3.4 函数 try 块

构造函数初始化列表抛异常时,普通的 try/catch 包不住,需要用函数 try 块:

cpp 复制代码
// 适用于 C++17

class Resource {

public:

    Resource(int fd) try : fd_{fd} {

        if (fd_ < 0) throw std::invalid_argument("bad fd");

    } catch (...) {

        // 记录日志后必须重新抛出,否则构造会「成功」但对象不完整

        throw;

    }



private:

    int fd_;

};

花括号在这里的放置位置是语法要求:try 在冒号之前,catch 在函数体之后。

四、语法消歧与空花括号

4.1 最令人头疼的解析

cpp 复制代码
// 适用于 C++17

class Timer {

public:

    Timer() = default;

};



Timer t1();    // 这是函数声明!返回 Timer,不接受参数

Timer t2{};    // 这是对象定义

Timer t3;      // 这也是对象定义

Timer t1(); 被解析成函数声明,这叫做 most vexing parse(MVP)。用 {} 或去掉括号都能避免。同样的坑出现在 std::vector<int> v(std::istream_iterator<int>(cin), ...) 这类嵌套括号场景。

4.2 auto 与花括号推导

cpp 复制代码
// 适用于 C++17

auto x = {1, 2, 3};   // std::initializer_list<int>

auto y{1};            // C++17 起推导为 int;C++11/14 为 initializer_list<int>

auto z = {1};         // std::initializer_list<int>

C++17 修改了 auto x{1} 的推导规则(提案 N3922),单元素且类型一致时推导为元素类型。跨标准写代码时不要依赖这个差异,显式写类型最稳。auto x{} 在 C++17 里无法推导,是 ill-formed。

4.3 返回与默认实参

return {}; 表示返回一个值初始化的对象,适合返回 std::optional、空容器、空字符串:

cpp 复制代码
// 适用于 C++17

#include <optional>

#include <string>



std::optional<int> find_id(const std::string& key) {

    if (key.empty()) return {};

    return 42;

}

默认实参也能用 {}:void log(std::string prefix = {});,等价于默认构造一个空串。

五、完整示例:几种花括号用法放在一起

cpp 复制代码
// 适用于 C++17

#include <cstddef>

#include <iostream>

#include <string>

#include <utility>

#include <vector>



struct Reading {

    std::string sensor;

    double value;

    long timestamp;

};



class Sampler {

public:

    explicit Sampler(std::size_t capacity) : capacity_{capacity} {

        readings_.reserve(capacity_);

    }



    void add(Reading r) {

        if (readings_.size() == capacity_) {

            readings_.erase(readings_.begin());

        }

        readings_.push_back(std::move(r));

    }



    double average() const {

        if (readings_.empty()) return {};

        double sum{};

        for (const auto& r : readings_) {

            sum += r.value;

        }

        return sum / static_cast<double>(readings_.size());

    }



private:

    std::size_t capacity_{};

    std::vector<Reading> readings_{};

};



int main() {

    Sampler sampler{3};

    sampler.add({"temp", 21.5, 1000});

    sampler.add({"temp", 22.0, 1001});

    sampler.add({"temp", 20.5, 1002});



    {

        // 独立的块作用域,演示变量生命周期

        std::vector<int> scratch{1, 2, 3};

        std::cout << scratch.size() << '\n';

    }



    std::cout << sampler.average() << '\n';

    return 0;

}

这份示例里出现了聚合初始化(Reading 的三个字段)、构造函数初始化列表(capacity_{capacity})、NSDMI(capacity_{})、函数返回 {}、块作用域和 vector 的列表初始化。注意 {"temp", 21.5, 1000} 作为 Reading 的实参时,外层花括号是构造实参,内层按聚合规则填字段。

常见坑点

  1. ❌ std::vector<int> v{10}; 以为得到 10 个零

✅ 要 10 个零写 std::vector<int> v(10); 有 initializer_list 构造函数时花括号优先,{10} 是一个元素。

  1. ❌ int x{3.9}; 以为会截断成 3

✅ 需要截断就写 int x = static_cast<int>(3.9); 浮点转整数在列表初始化里是窄化,标准要求编译错误。

  1. ❌ Timer t(); 以为定义了一个对象

✅ Timer t{}; 这是函数声明(most vexing parse),不是变量定义。

  1. ❌ 构造函数初始化列表按书写顺序理解成员初始化顺序

✅ 按成员声明顺序写,或让初始化表达式互不依赖 实际顺序由声明顺序决定,跨成员依赖会读到未初始化值。

  1. ❌ auto x{1}; 在所有标准下都当 initializer_list

✅ 需要列表就写 auto x = {1};,需要单值就显式写类型 C++17 起 auto x{1} 推导为 int,与 C++11/14 不同。

  1. ❌ 用 if (cond) 包多条语句时省略花括号

✅ 始终写 { } 省略后插入一条语句就会改变控制流,宏展开时尤其危险。

  1. ❌ 聚合初始化时省略内层花括号 Line l{0, 0, 10, 10, 0xFF0000}

✅ Line l{``{0, 0}, {10, 10}, 0xFF0000} 花括号省略合法但脆弱,结构一改就静默错位。

  1. ❌ 在 C++17 里写 Point p{.x = 1, .y = 2}

✅ C++17 用位置初始化 Point p{1, 2};指定初始化器需要 C++20 指定初始化器是 C++20 引入的,旧标准不支持。

总结

语境 花括号的作用 关键规则

|-------|--------------|---------------|
| 变量初始化 | 列表初始化 / 值初始化 | 拒绝窄化;{} 为零值 |

|-------|-------------------------|--------------|
| 类类型构造 | 优先匹配 initializer_list | 与圆括号语义可能完全不同 |

|------|----------|-------------|
| 聚合类型 | 按声明顺序填成员 | 可嵌套、可省略但别省略 |

|------|---------|-----------|
| 构造函数 | 初始化列表分隔 | 实际顺序看声明顺序 |

|----|-------|-------------|
| 裸块 | 引入作用域 | 常用于缩短锁的持有时间 |

|--------|-----|----------|
| lambda | 函数体 | 捕获列表用方括号 |

|--------|--------|-----------------------------|
| auto | 推导规则特殊 | auto x{1} 在 C++17 是 int |

花括号不是圆括号的同义词:它在初始化时更严格(挡窄化),在重载决议时更「霸道」(抢 initializer_list),在语法上还能消除函数声明歧义。把这三条记住,再遇到 vector<int> v{10} 之类的输出不符预期时,就能第一时间定位到重载决议而不是怀疑编译器。写新代码时,建议默认用 {} 做初始化,只有在明确要调用非列表构造函数时才退回圆括号。

相关推荐
xiaoye-duck16 分钟前
《C++ 项目:手写大模型接入 SDK》本地 Ollama 部署与适配
开发语言·网络·c++·语言模型
联犀38 分钟前
联犀 Go SIMD 实战:视频转码从 16.5ms 到 5.46ms
开发语言·golang·音视频
octopus_c1 小时前
STL 之 string:auto 与范围 for、常用接口、模拟实现与深浅拷贝
开发语言·c++·算法
Nerd(BUG版)1 小时前
KMP学习day5--Kotlin Flow异步流基础
开发语言·学习·kotlin
J.YA1 小时前
String字符串[面试题]
java·开发语言
汉克老师11 小时前
CSP-J 2026 第二轮入门组真题解析(第一题 、旅游)
c++·csp-j·信息学竞赛·小学生学c++
xiaoye-duck12 小时前
《C++ 项目:手写大模型接入 SDK》Gemini接口设计(上):初始化、全量响应实现和测试
开发语言·网络·c++·语言模型
nice_lcj52014 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
码农小河6615 小时前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql