Effective C++ 条款28:避免使用 handles 指向对象内部
2026/6/14 8:54:54 网站建设 项目流程

Effective C++ 条款28:避免使用 handles 指向对象内部

避免返回 handles(包括引用、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生"虚吊号码牌"(dangling handles)的可能性降至最低。

一、什么是 handles?

在 C++ 中,handles是指用于访问对象内部数据的机制,主要包括:

Handle 类型示例风险等级
引用(Reference)int& getData()
指针(Pointer)int* getData()
迭代器(Iterator)std::vector<int>::iterator begin()

返回 handles 指向对象内部成分,相当于把对象的内部实现细节暴露给了外部,这会破坏封装性。

二、封装性破坏的典型案例

2.1 基本示例:矩形类

// ❌ 不好的设计:返回内部成员的引用classPoint{public:Point(intxVal,intyVal):x(xVal),y(yVal){}// 危险!暴露了内部数据的可写引用int&getX(){returnx;}int&getY(){returny;}private:intx,y;};classRectangle{public:Rectangle(constPoint&ul,constPoint&lr):upperLeft(ul),lowerRight(lr){}// ❌ 返回内部成员的引用,封装性被破坏Point&getUpperLeft(){returnupperLeft;}Point&getLowerRight(){returnlowerRight;}private:Point upperLeft;Point lowerRight;};// 客户端代码Rectanglerect(Point(0,0),Point(100,100));// 可以直接修改矩形内部状态!封装性完全被破坏了rect.getUpperLeft().getX()=50;// 修改了私有成员!rect.getUpperLeft()=Point(10,10);// 直接替换了私有成员!

2.2 更好的设计:返回副本或 const 引用

// ✅ 好的设计:保持封装性classPoint{public:Point(intxVal,intyVal):x(xVal),y(yVal){}// 返回值的副本(对于小对象)intgetX()const{returnx;}intgetY()const{returny;}// 或者提供受控的修改接口voidsetX(intnewX){x=newX;}voidsetY(intnewY){y=newY;}private:intx,y;};classRectangle{public:Rectangle(constPoint&ul,constPoint&lr):upperLeft(ul),lowerRight(lr){}// ✅ 返回副本,完全安全PointgetUpperLeft()const{returnupperLeft;}PointgetLowerRight()const{returnlowerRight;}// 提供受控的修改接口voidsetUpperLeft(constPoint&p){upperLeft=p;}voidsetLowerRight(constPoint&p){lowerRight=p;}private:Point upperLeft;Point lowerRight;};

三、const 正确性问题

3.1 问题演示

classString{public:String(constchar*str):data(str){}// ❌ 问题:const 成员函数返回了非 const 引用char&operator[](size_t index)const{returndata[index];// 返回了内部数据的非 const 引用}private:char*data;};// 客户端代码constStringgreeting("Hello");// greeting 是 const,理论上不应该被修改// 但通过返回的引用,我们可以修改它!greeting[0]='J';// 现在 greeting 变成了 "Jello"!// 这违反了 const 的正确性:const 对象被修改了!

3.2 解决方案:const 重载

// ✅ 正确的做法:提供 const 和非 const 两个版本classString{public:String(constchar*str):data(newchar[strlen(str)+1]){strcpy(data,str);}~String(){delete[]data;}// 非 const 版本:返回非 const 引用char&operator[](size_t index){returndata[index];}// const 版本:返回 const 引用(或值)constchar&operator[](size_t index)const{returndata[index];}private:char*data;};// 现在 const 正确性得到了保证StringmutableStr("Hello");mutableStr[0]='J';// ✅ 正确:非 const 对象可以被修改constStringconstStr("Hello");// constStr[0] = 'J'; // ❌ 编译错误!const 对象不能被修改

3.3 返回 const 引用的情况

对于较大的对象,返回副本可能效率低下。此时可以返回 const 引用,但要确保引用的生命周期:

classImage{public:Image(intw,inth):width(w),height(h),pixels(w*h){}// ✅ 返回 const 引用:对于大对象效率更高conststd::vector<Pixel>&getPixels()const{returnpixels;}// ❌ 不要返回非 const 引用// std::vector<Pixel>& getPixels() { return pixels; }// 如果需要修改,提供受控接口voidsetPixel(intx,inty,constPixel&p){pixels[y*width+x]=p;}private:intwidth,height;std::vector<Pixel>pixels;};

四、悬空 handles(Dangling Handles)

4.1 什么是悬空 handles?

当返回的 handle 所指向的对象被销毁后,该 handle 就变成了"悬空"的,继续使用它会导致未定义行为。

classWidget{public:Widget(intv):value(v){}// ❌ 危险:返回内部成员的引用int&getValue(){returnvalue;}private:intvalue;};// 悬空引用示例int&getWidgetValue(){Widgetw(42);// 局部对象returnw.getValue();// 返回局部对象的引用!}// w 在这里被销毁,返回的引用悬空!// 使用int&val=getWidgetValue();// val 现在是悬空引用!// std::cout << val; // 未定义行为!可能崩溃、可能输出垃圾值

4.2 更隐蔽的悬空问题

classContainer{public:voidadd(intval){data.push_back(val);}// ❌ 返回内部 vector 元素的引用int&get(size_t index){returndata[index];}// 任何可能导致 vector 重新分配的操作voidreserve(size_t n){data.reserve(n);}private:std::vector<int>data;};Container c;c.add(1);c.add(2);int&ref=c.get(0);// 获取第一个元素的引用std::cout<<ref<<"\n";// 输出 1c.reserve(100);// 可能导致 vector 重新分配内存!// ref 现在可能悬空!因为 vector 可能搬家了std::cout<<ref<<"\n";// 未定义行为!

4.3 字符串处理的经典陷阱

classPerson{public:Person(conststd::string&name):name_(name){}// ❌ 极其危险:返回内部 string 的 c_str()constchar*getName()const{returnname_.c_str();// 返回指向内部数据的指针}// 任何修改 name_ 的操作voidsetName(conststd::string&name){name_=name;}private:std::string name_;};Personperson("Alice");constchar*name=person.getName();std::cout<<name<<"\n";// 输出 Aliceperson.setName("Bob");// 修改了内部 string// name 现在可能悬空!string 可能重新分配了内存std::cout<<name<<"\n";// 未定义行为!

五、实际应用场景

场景1:矩阵类的设计

// ❌ 不好的设计classMatrix{public:Matrix(introws,intcols):rows_(rows),cols_(cols),data_(rows*cols){}// 危险!返回内部数据的引用double&at(introw,intcol){returndata_[row*cols_+col];}std::vector<double>&getData(){returndata_;}private:introws_,cols_;std::vector<double>data_;};// ✅ 好的设计classMatrix{public:Matrix(introws,intcols):rows_(rows),cols_(cols),data_(rows*cols){}// 返回值的副本(对于单个元素)doubleat(introw,intcol)const{returndata_[row*cols_+col];}// 提供受控的修改接口voidset(introw,intcol,doublevalue){data_[row*cols_+col]=value;}// 如果需要批量访问,提供安全的访问器classRowAccessor{public:RowAccessor(Matrix&m,introw):matrix_(m),row_(row){}doubleoperator[](intcol)const{returnmatrix_.at(row_,col);}voidset(intcol,doublevalue){matrix_.set(row_,col,value);}private:Matrix&matrix_;introw_;};RowAccessoroperator[](introw){returnRowAccessor(*this,row);}// 返回 const 引用(只读访问)conststd::vector<double>&data()const{returndata_;}private:introws_,cols_;std::vector<double>data_;};// 使用Matrixm(3,3);m.set(0,0,1.0);m[0].set(1,2.0);doubleval=m.at(0,0);// 安全访问

场景2:配置管理器

// ❌ 不好的设计:返回内部 map 的引用classConfig{public:voidload(conststd::string&file);// 危险!外部可以直接修改内部配置std::map<std::string,std::string>&getSettings(){returnsettings_;}private:std::map<std::string,std::string>settings_;};// ✅ 好的设计classConfig{public:voidload(conststd::string&file);// 安全的访问方式std::stringget(conststd::string&key,conststd::string&defaultVal="")const{autoit=settings_.find(key);return(it!=settings_.end())?it->second:defaultVal;}voidset(conststd::string&key,conststd::string&value){settings_[key]=value;}boolhas(conststd::string&key)const{returnsettings_.find(key)!=settings_.end();}// 如果需要遍历,提供受控的迭代器访问usingconst_iterator=std::map<std::string,std::string>::const_iterator;const_iteratorbegin()const{returnsettings_.begin();}const_iteratorend()const{returnsettings_.end();}private:std::map<std::string,std::string>settings_;};

场景3:数据库结果集

// ❌ 不好的设计classQueryResult{public:// 返回内部行的引用,可能导致悬空Row&getRow(size_t index){returnrows_[index];}// 返回内部数据的指针constchar*getValue(size_t row,size_t col){returnrows_[row][col].c_str();}private:std::vector<Row>rows_;};// ✅ 好的设计classQueryResult{public:// 返回值的副本(字符串)std::stringgetValue(size_t row,size_t col)const{returnrows_[row][col];}// 或者使用 string_view(C++17)进行零拷贝只读访问std::string_viewgetValueView(size_t row,size_t col)const{returnrows_[row][col];}// 提供安全的遍历接口voidforEachRow(std::function<void(constRow&)>callback)const{for(constauto&row:rows_){callback(row);}}size_trowCount()const{returnrows_.size();}size_tcolCount()const{returnrows_.empty()?0:rows_[0].size();}private:std::vector<Row>rows_;};

六、例外情况

6.1 operator[] 的特殊性

// 对于容器类,operator[] 通常需要返回引用以支持赋值classArray{public:double&operator[](size_t index){returndata_[index];}constdouble&operator[](size_t index)const{returndata_[index];}private:std::vector<double>data_;};// 这是合理的,因为:// 1. 这是容器的标准接口// 2. 用户期望 arr[i] = 42 能工作// 3. 提供了 const 版本保证 const 正确性

6.2 智能指针和代理对象

// 使用代理对象来安全地暴露内部数据classSafeContainer{public:classProxy{public:Proxy(SafeContainer&c,size_t idx):container_(c),index_(idx){}// 支持读取operatorint()const{returncontainer_.data_[index_];}// 支持写入(可以添加验证逻辑)Proxy&operator=(intvalue){if(value<0){throwstd::invalid_argument("Negative values not allowed");}container_.data_[index_]=value;return*this;}private:SafeContainer&container_;size_t index_;};Proxyoperator[](size_t index){returnProxy(*this,index);}intget(size_t index)const{returndata_[index];}private:std::vector<int>data_;};SafeContainer sc;sc[0]=42;// 通过代理对象安全赋值intval=sc[0];// 通过代理对象读取

6.3 PIMPL 惯用法中的 handles

// PIMPL(Pointer to Implementation)惯用法中,// 返回 impl 指针是合理的,因为 impl 的生命周期由外部对象管理classWidget{public:Widget();~Widget();// 这是合理的:impl 的生命周期与 Widget 绑定WidgetImpl*getImpl(){returnimpl_.get();}private:std::unique_ptr<WidgetImpl>impl_;};

七、总结与最佳实践

原则说明
避免返回非 const 引用/指针这会破坏封装性,允许外部直接修改内部状态
const 成员函数返回 const 引用/值保持 const 正确性
警惕悬空 handles确保返回的 handle 不会比对象本身活得更长
提供受控的修改接口通过 setter 方法控制对内部数据的修改
考虑返回副本对于小对象,返回副本是最安全的选择
使用代理对象在需要灵活访问时使用代理模式

请记住:

  • 避免返回 handles(引用、指针、迭代器)指向对象内部。
  • 遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const。
  • 将发生"虚吊号码牌"(dangling handles)的可能性降至最低。

参考阅读:

  • 《Effective C++》第三版,条款28
  • 《C++ Primer》关于封装和 const 正确性的章节
  • C++ Core Guidelines: F.16, F.17, Con.1

如果这篇文章对你有帮助,欢迎点赞、收藏和转发!有任何问题欢迎在评论区留言讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询