尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++11 新特性系列(二):初始化与空指针的现代化

C++11 新特性系列(二):初始化与空指针的现代化 C11 新特性系列二初始化与空指针的现代化本系列第二篇我们聚焦 C11 在初始化和空指针方面的革命性改进。统一初始化语法终结了以往混乱的初始化方式而nullptr则彻底告别了 NULL 带来的二义性噩梦。一起看看这些改变如何让代码更安全、更一致。从“百花齐放”到“一统天下”初始化方式的演进在 C98/03 中初始化一个变量或对象你有至少四种不同的写法具体用哪个取决于类型和上下文inta10;// 赋值初始化intb(10);// 构造函数初始化intc{10};// 列表初始化仅对 POD 和数组intd{10};// 直接列表初始化C98 对聚合体有效对于类对象更是一片混乱std::vectorintv1(10,5);// 10 个 5std::vectorintv2{1,2,3};// 在 C98 中编译错误没有 initializer_listintarr[]{1,2,3};// 只能用列表初始化数组structPoint{intx,y;};Point p{1,2};// 聚合体初始化Point q{3,4};// 也可以更让人头疼的是std::vectorint(10, 5)和std::vectorint{10, 5}的意义完全不同——前者是 10 个 5后者是包含 10 和 5 两个元素的 vector。这种不一致性容易导致误用和 bug。C11 引入统一初始化uniform initialization即使用花括号{}初始化一切让语法变得一致同时提供了std::initializer_list支持自定义类型的列表构造。与此同时空指针的表示也长期存在隐患——NULL通常被定义为0或(void*)0在重载场景下会导致二义性甚至被误当作整数。C11 引入nullptr彻底解决了这个问题。一、统一初始化语法列表初始化1.1 基本用法一切皆可{}C11 允许使用花括号{}来初始化任何类型的变量无论是内置类型、数组、聚合体还是类对象// 内置类型inta{10};// 等价于 int a 10;doubleb{3.14};charc{A};// 数组intarr[]{1,2,3,4};// 标准库容器std::vectorintvec{1,2,3,4,5};std::mapstd::string,intscores{{Alice,95},{Bob,87}};// 自定义类聚合体structPoint{intx;inty;};Point p{10,20};// 自定义类带构造函数的类classMyClass{public:MyClass(inta,doubleb):m_a(a),m_b(b){}private:intm_a;doublem_b;};MyClass obj{42,3.14};// 调用构造函数甚至可以用{}进行动态内存分配int*pnewint{100};std::vectorint*vpnewstd::vectorint{1,2,3};1.2 防止窄化转换Narrowing Conversion统一初始化最实用的特性之一是禁止窄化转换narrowing conversion。所谓窄化是指隐式转换导致精度丢失或范围缩小的情形。inta3.14;// 传统方式警告但允许a 变成 3intb{3.14};// 错误窄化转换编译不通过charc11000;// 允许可能溢出charc2{1000};// 错误窄化转换1000 超出 char 范围doubled0;int*pd;// 错误但有些编译器只警告int*q{d};// 错误窄化转换double 到指针这个特性在初始化大型对象或模板时尤其有用因为它能在编译期捕获意外的精度损失让代码更健壮。1.3 初始值设定空列表使用空{}可以值初始化value-initialize变量对于内置类型这会将它们清零inta{};// a 0doubleb{};// b 0.0int*p{};// p nullptr注意不是 NULLstructS{intx;doubley;};S s{};// s.x 0, s.y 0.0std::vectorintvec{};// 空 vector这提供了一种安全、简洁的零初始化方式避免了未初始化变量的风险。二、std::initializer_list支持列表初始化的类2.1 什么是 initializer_list要想让自定义类型也支持像std::vectorint{1,2,3}这样的列表初始化需要用到std::initializer_listT。这是一个轻量级的代理类表示一个由编译器自动构建的临时数组。类可以提供一个接受std::initializer_list参数的构造函数或者使用重载赋值运算符接受它。2.2 实现一个支持列表初始化的类#includeiostream#includeinitializer_list#includevectorclassMyArray{public:// 接受 initializer_list 的构造函数MyArray(std::initializer_listintlist){for(autoitlist.begin();it!list.end();it){data.push_back(*it);}}// 也可以重载赋值运算符MyArrayoperator(std::initializer_listintlist){data.clear();for(autoelem:list){data.push_back(elem);}return*this;}voidprint()const{for(intval:data)std::coutval ;std::coutstd::endl;}private:std::vectorintdata;};intmain(){MyArray arr1{1,2,3,4};// 调用构造函数arr1.print();// 1 2 3 4MyArray arr2{5,6,7};// 也 OK构造arr2.print();// 5 6 7arr1{8,9};// 调用赋值运算符arr1.print();// 8 9return0;}2.3 优先匹配规则当一个类既有普通构造函数又有initializer_list构造函数时如果使用{}初始化编译器会优先匹配initializer_list版本classFoo{public:Foo(inta,intb){std::coutFoo(int,int)\n;}Foo(std::initializer_listint){std::coutFoo(initializer_list)\n;}};Foo f1{1,2};// 调用 initializer_list 版本输出 Foo(initializer_list)Foof2(1,2);// 调用普通构造函数输出 Foo(int,int)这解释了为什么std::vectorint{10, 5}表示两个元素而std::vectorint(10, 5)表示 10 个 5——因为{}触发了initializer_list构造函数。陷阱如果initializer_list构造函数的参数类型可以匹配它会“劫持”初始化即使这看起来像是用两个整数构造Foo f3{10};// 如果 Foo 有 Foo(int) 和 Foo(initializer_listint)则调用后者用 {10} 构造 initializer_list所以设计类时需注意如果提供了initializer_list构造函数要确保它与其他构造函数的语义不冲突。三、nullptr真正的空指针3.1 NULL 的痛点在 C98 中NULL通常被定义为0某些实现为(void*)0但 C 不允许隐式转换void*到其他指针类型所以大多数实现用0。这就带来了几个问题问题1二义性重载voidfunc(int){std::coutint\n;}voidfunc(char*){std::coutchar*\n;}func(NULL);// 调用 int 版本因为 NULL 是 0程序员的本意往往是调用指针版本结果却调用了整数版本而且编译器不会报错容易产生隐蔽的逻辑错误。问题2在模板中误判为整数templatetypenameTvoidprocess(T*ptr){/* ... */}process(NULL);// 推导 T int 错误NULL 是 int无法作为指针参数问题30本身作为整数常量在类内初始化时语义模糊3.2 nullptr 的本质C11 引入nullptr它是一个空指针常量类型是std::nullptr_t定义在cstddef。nullptr可以隐式转换为任何指针类型和成员指针类型但不能转换为整数类型除了bool。int*p1nullptr;// OKchar*p2nullptr;// OKvoid*p3nullptr;// OKint(MyClass::*pmf)()nullptr;// OK成员函数指针intinullptr;// 错误不能转换为整数nullptr与0的比较if(ptrnullptr){/* ... */}// 清晰意图if(ptr0){/* ... */}// 较不清晰3.3 解决重载二义性有了nullptr上面的重载示例会正确调用指针版本voidfunc(int){std::coutint\n;}voidfunc(char*){std::coutchar*\n;}func(nullptr);// 调用 char* 版本func(0);// 调用 int 版本0 是 int 字面量3.4 std::nullptr_t如果需要存储或传递nullptr可以使用std::nullptr_t类型#includecstddefvoidlog(std::nullptr_t){std::coutGot nullptr\n;}intmain(){log(nullptr);// OK// log(0); // 错误std::nullptr_t npnullptr;int*pnp;// OK隐式转换return0;}注意不要重载std::nullptr_t版本与其他指针类型否则也会导致二义性因为nullptr可转换为任何指针编译器无法区分。四、default 与 delete控制默认函数4.1 特殊成员函数的自动生成在 C98 中如果用户没有声明编译器会为类自动生成默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。但一旦用户自定义了任一构造函数默认构造函数就不再自动生成。这一规则有时会造成困扰。C11 引入了default和delete让程序员显式控制这些特殊成员函数的生成。4.2 default显式要求生成默认版本default告诉编译器请使用默认实现。classWidget{public:Widget()default;// 显式生成默认构造函数Widget(constWidget)default;// 显式生成拷贝构造函数Widgetoperator(constWidget)default;// 显式生成拷贝赋值// 或者简写只需声明即可编译器会生成~Widget()default;};适用场景当你需要自定义某个构造函数但仍希望保留默认构造函数时。当你因为某些原因如定义了移动构造函数导致拷贝构造函数被抑制但你仍希望保留它们时注意移动构造会抑制拷贝构造但可以用default恢复。期望获得比手写版本更好的性能编译器默认实现可能更高效。示例classMyClass{public:MyClass(intx):m_x(x){}// 自定义构造函数MyClass()default;// 仍然保留默认构造函数private:intm_x;};MyClass a;// OK使用默认构造MyClassb(10);// OK使用自定义构造4.3 delete禁止特定函数delete让程序员显式删除某个函数使得任何试图调用它的代码都会在编译期报错。它比将函数声明为private且不实现的方式更彻底后者在链接期报错且信息不友好。classNonCopyable{public:NonCopyable()default;NonCopyable(constNonCopyable)delete;// 禁止拷贝构造NonCopyableoperator(constNonCopyable)delete;// 禁止拷贝赋值};NonCopyable a;// NonCopyable b(a); // 错误拷贝构造被删除// NonCopyable c; c a; // 错误拷贝赋值被删除可以删除任意函数不仅限于特殊成员voidfunc(int){/* ... */}voidfunc(double)delete;// 禁止调用 double 版本func(10);// OK// func(3.14); // 错误删除函数还可以删除模板特化templatetypenameTvoidprocess(T){/* ... */}templatevoidprocessvoid(void)delete;// 禁止对 void 的调用删除析构函数可以阻止栈上对象创建但需要结合动态内存管理classSingleton{public:staticSingletongetInstance(){staticSingleton instance;returninstance;}Singleton(constSingleton)delete;Singletonoperator(constSingleton)delete;private:Singleton()default;~Singleton()delete;// 禁止外部 delete};// 需要谨慎使用通常不推荐删除析构函数4.4 移动语义对默认函数的影响简要提及在 C11 中如果用户声明了移动构造函数或移动赋值运算符那么拷贝构造函数和拷贝赋值运算符会被隐式删除。如果仍需要它们可以使用default恢复。这涉及到移动语义我们将在系列第四篇详细讲解。综合示例将所有新特性融合在一个场景中一个支持列表初始化的智能数组类并且禁止拷贝仅支持移动移动语义后续会讲这里先展示 delete 和 default 的运用以及 initializer_list。#includeiostream#includeinitializer_list#includealgorithmclassIntArray{public:// 使用 initializer_list 构造IntArray(std::initializer_listintlist):m_size(list.size()),m_data(newint[list.size()]){std::copy(list.begin(),list.end(),m_data);std::coutConstructed from initializer_list\n;}// 默认构造IntArray():m_size(0),m_data(nullptr){}// 析构~IntArray(){delete[]m_data;}// 禁止拷贝但允许移动移动语义后面会讲这里先占位IntArray(constIntArray)delete;IntArrayoperator(constIntArray)delete;// 允许默认移动C11 自动生成但前提是未声明拷贝构造这里我们显式声明了 delete所以移动也会被抑制需要显式 default// 但为了演示我们暂时不展开移动只是说明 delete 的用法voidprint()const{for(size_t i0;im_size;i)std::coutm_data[i] ;std::coutstd::endl;}private:size_t m_size;int*m_data;};intmain(){// 统一初始化inta{42};int*p{nullptr};// 使用 initializer_list 构造IntArray arr{1,2,3,4,5};arr.print();// 1 2 3 4 5// IntArray arr2 arr; // 错误拷贝构造被删除// 使用 nullptrif(pnullptr){std::coutp is null\n;}return0;}常见陷阱与最佳实践陷阱1{}初始化与initializer_list的偏好如前所述{}会优先匹配initializer_list构造函数这可能导致意外行为std::vectorintv1{10,5};// 两个元素10 和 5std::vectorintv2(10,5);// 10 个 5在设计类时如果要避免这种歧义可以提供initializer_list构造函数但也要确保其他构造函数的使用方式清晰。陷阱2auto与{}的交互auto与{}结合时推导结果可能出乎意料autox1{10};// 在 C11 中推导为 std::initializer_listint// 实际上C11 中 auto 用 {} 初始化会推导为 initializer_list但有歧义// 自 C17 起规则改变为推导为 int。// 本系列基于 C11建议避免用 auto x {10}; 这样的写法。更好的做法是用auto时尽量用或()初始化而非{}以避免歧义。或者明确使用auto x 10;。陷阱3nullptr与bool的转换nullptr可以隐式转换为bool因此在条件表达式中可以用if(ptr){/* ... */}// ptr 是 nullptr 则为 false但这不意味着nullptr可以参与算术运算它本质上是空指针常量。最佳实践总结优先使用{}初始化尤其是对于容器和聚合体但注意与initializer_list构造函数的优先级。使用nullptr替代NULL和0表示空指针。使用default生成默认特殊成员让编译器优化。使用delete明确禁止不需要的函数尤其是拷贝和移动操作这比private声明更优雅、更安全。对于类遵循“三五法则”如果定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个通常需要定义所有三个在 C11 后还需考虑移动利用default/delete来显式控制。总结特性核心作用关键点统一初始化{}一致的初始化语法适用于任何类型禁止窄化转换会优先匹配initializer_list构造函数std::initializer_list让自定义类型支持列表初始化提供带initializer_list的构造/赋值函数nullptr类型安全的空指针常量可转为任意指针不能转为整数除 booldefault显式要求编译器生成默认版本用于特殊成员函数delete禁止调用某个函数可应用于任何函数不仅是特殊成员这一篇的内容让 C 的初始化行为和空指针语义变得现代化、一致化且更安全。它们看似简单却在日常编码中频繁使用深刻影响着代码的可读性和健壮性。下一篇预告我们将探讨让代码更清晰的语法糖包括范围for循环、override/final关键字、委托构造函数和继承构造函数。这些特性将进一步简化类继承和循环代码让 C 更加易用。敬请期待
返回列表