C++ 成员函数完全指南:每一种函数都有它的使命
C 成员函数完全指南每一种函数都有它的使命类的成员函数定义了对象的行为。和成员变量一样C 提供了多种不同性质的成员函数普通成员函数、const 成员函数、静态成员函数、虚函数、纯虚函数、运算符重载、以及 C11 引入的特殊成员函数……每一种都有独特的语义和规则。今天我们就来拆解这个函数家族。1. 普通成员函数最基础的行为定义classWidget{intvalue;public:voidsetValue(intv){valuev;}// 普通成员函数intgetValue(){returnvalue;}// 普通成员函数};特点有一个隐式的this指针指向调用对象可以访问对象的所有成员包括 private可以修改对象状态只能被非 const 对象调用Widget w;w.setValue(10);// this 指向 wconstWidget cw;// cw.getValue(); // 错误const 对象不能调用非 const 成员函数2. const 成员函数承诺不修改对象classWidget{intvalue;public:intgetValue()const{// const 成员函数returnvalue;// 可以读// value 10; // 错误不能修改非 mutable 成员}};核心要点语法在参数列表后加constthis指针类型变为const Widget* const不能修改非mutable的成员变量可以被 const 对象调用可以与普通版本构成重载classWidget{public:voiddisplay(){std::coutnon-const\n;}voiddisplay()const{std::coutconst\n;}};Widget w;w.display();// 输出non-const优先匹配非 const 版本constWidget cw;cw.display();// 输出const只能匹配 const 版本设计准则不修改对象状态的成员函数一律标记为const。这是接口设计的重要信号。3. 静态成员函数属于类不属于对象classMath{public:staticintabs(intx){// 静态成员函数returnx0?-x:x;}staticintinstanceCount(){// 静态成员函数returncount;// 只能访问静态成员}private:staticintcount;// int value; // 静态函数不能访问这个};intMath::count0;// 调用方式intresultMath::abs(-5);// 通过类名调用Math m;intresult2m.abs(-5);// 也可以通过对象调用不推荐特点没有this指针只能访问静态成员变量和其他静态成员函数不能是 const因为没有 thisconst 修饰没意义不能是虚函数没有 this无法动态绑定典型用途工厂方法std::make_unique、std::make_shared工具函数数学计算、字符串处理访问全局状态实例计数、配置管理4. 虚函数多态的灵魂classAnimal{public:virtualvoidspeak()const{// 虚函数std::coutAnimal sound\n;}virtual~Animal()default;// 虚析构};classDog:publicAnimal{public:voidspeak()constoverride{// 重写虚函数std::coutWoof!\n;}};// 多态使用voidmakeSound(constAnimala){a.speak();// 运行时决定调用哪个版本}Dog dog;makeSound(dog);// 输出Woof!调用 Dog::speak不是 Animal::speak虚函数 允许子类重写实现运行时多态基类声明虚函数派生类可以重写 (override) 这个函数调用时看对象真实类型不看指针 / 引用类型触发动态绑定晚绑定程序运行时才确定调用哪个版本类内部自动生成虚函数表 (vtable)存所有虚函数地址有虚函数的类不能直接当成纯数据结构体内存多一个虚表指针虚函数的工作原理类有虚函数 → 有虚函数表vtable每个对象有虚函数表指针vptr指向 vtable通过基类指针/引用调用虚函数时运行时根据 vptr 找到正确版本的函数关键规则基类中声明为virtual派生类中自动是虚的override关键字可选但强烈推荐构造函数不能是虚的析构函数如果是基类必须是虚的普通析构静态绑定编译期只看指针类型父类虚析构动态绑定运行时看真实对象类型子类静态成员函数不能是虚的classA{~A(){}};// 普通析构非虚classB:publicA{~B(){}};A*pnewB;//出现错误由于A仅静态绑定了A的析构函数deletep;classA{virtual~A(){}};// 虚析构动态绑定保证父子析构全都执行5. 纯虚函数与抽象类classShape{// 抽象类public:virtualdoublearea()const0;// 纯虚函数virtualvoiddraw()const0;// 纯虚函数virtual~Shape()default;// 虚析构};classCircle:publicShape{doubleradius;public:doublearea()constoverride{// 必须实现return3.14159*radius*radius;}voiddraw()constoverride{// 必须实现// 绘制圆形}};// Shape s; // 错误不能实例化抽象类Shape*snewCircle();// 正确特点包含纯虚函数的类是抽象类不能实例化派生类必须实现所有纯虚函数否则也是抽象类纯虚函数可以有实现但很少需要classBase{public:virtualvoidinterface()0;// 纯虚函数};// 可以提供默认实现voidBase::interface(){std::coutDefault implementation\n;}6. override 与 final现代 C 的安全保障6.1 override显式声明重写classBase{public:virtualvoidf(int)const;};classDerived:publicBase{public:voidf(int)constoverride;// 正确显式重写// void f(int) override; // 编译错误缺少 const签名不匹配// void f(double) override; // 编译错误参数类型不匹配};为什么要用 override让编译器帮你检查是否真的重写了基类虚函数如果基类修改了函数签名派生类会立即编译报错不使用override标注时写错签名编译器不报错悄悄变成隐藏 / 普通函数丢失重写提高代码可读性明确表达意图6.2 final阻止进一步重写或继承锁住核心逻辑不让子类乱改保证底层行为永远固定父类写好底层关键逻辑标记final。子类只能调用不能篡改核心实现。避免子类乱重写导致业务出错、逻辑混乱。// final 用于虚函数阻止派生类重写classBase{public:virtualvoidf()final;// 派生类不能重写 f()};classDerived:publicBase{public:// void f() override; // 编译错误f 是 final};// final 用于类阻止继承classFinalClassfinal{// 不能被继承};// class CannotInherit : public FinalClass {}; // 编译错误7. 特殊成员函数编译器帮你生成的前面构造函数和析构函数文章中已经详细讲过这里快速回顾classWidget{public:Widget()default;// 默认构造~Widget()default;// 析构Widget(constWidget)default;// 拷贝构造Widgetoperator(constWidget)default;// 拷贝赋值Widget(Widget)noexceptdefault;// 移动构造 (C11)Widgetoperator(Widget)noexceptdefault;// 移动赋值 (C11)};这些是编译器能自动生成的函数。用 default显式要求生成 delete显式禁止。classNonCopyable{public:NonCopyable()default;NonCopyable(constNonCopyable)delete;// 禁止拷贝NonCopyableoperator(constNonCopyable)delete;// 禁止拷贝赋值};8. 运算符重载让你的类像内置类型classComplex{doublereal,imag;public:Complex(doubler0,doublei0):real(r),imag(i){}// 二元运算符 加法Complexoperator(constComplexother)const{returnComplex(realother.real,imagother.imag);}// 复合赋值运算符Complexoperator(constComplexother){realother.real;imagother.imag;return*this;}// 一元运算符- 取负Complexoperator-()const{returnComplex(-real,-imag);}// 比较运算符booloperator(constComplexother)const{returnrealother.realimagother.imag;}// 流输出友元函数friendstd::ostreamoperator(std::ostreamos,constComplexc);};std::ostreamoperator(std::ostreamos,constComplexc){returnosc.real c.imagi;}// 使用Complexa(1,2),b(3,4);Complex cab;// 像内置类型一样自然ab;// 复合赋值Complex d-a;// 一元运算符常用可重载的运算符类型运算符典型返回值算术 - * / %新对象值复合赋值 - * /*this引用比较 ! bool一元- ! ~新对象或引用下标[]引用通常两个版本函数调用()任意仿函数类型转换operator T()目标类型流 ostream / istream友元设计准则算术运算符通常返回新对象const值复合赋值返回*this引用[]通常提供 const 和非 const 两个版本流运算符用友元函数非成员函数9. 成员函数与 free 函数的抉择有些操作适合作为成员函数有些更适合作为普通函数classString{public:// 成员函数需要访问私有成员size_tlength()const;charoperator[](size_t index);// 也可以作为成员但可能更好做 free 函数boolcontains(constStringsubstr)const;};// free 函数不需要特殊访问权限booloperator(constStringa,constStringb);// 对称性好std::ostreamoperator(std::ostreamos,constStrings);// 必须是 free// 设计准则如果一个操作需要访问私有成员做成成员函数或友元// 如果可以只通过公有接口实现做成 free 函数降低耦合10. C11/14/17 新特性与成员函数10.1 移动语义相关的成员函数classBuffer{char*data;size_t size;public:// 移动构造函数Buffer(Bufferother)noexcept:data(other.data),size(other.size){other.datanullptr;other.size0;}// 移动赋值运算符Bufferoperator(Bufferother)noexcept{if(this!other){delete[]data;dataother.data;sizeother.size;other.datanullptr;other.size0;}return*this;}};10.2 右值引用限定的成员函数C11classWidget{std::vectorintdata;public:// 左值引用限定只能被左值调用std::vectorintgetData(){returndata;}// 右值引用限定只能被右值调用std::vectorintgetData(){returnstd::move(data);// 安全地移动因为对象即将被销毁}};Widget w;autod1w.getData();// 调用左值版本返回引用autod2Widget().getData();// 调用右值版本移动数据10.3 noexcept 说明符C11classSafe{public:voidriskyOperation()noexcept{// 承诺不抛异常// ...}voidmayThrow();// 可能抛异常};移动操作强烈建议标记noexcept这样标准库容器才能在需要时安全使用它们。10.4 auto 返回类型推导C14classCalculator{public:autoadd(inta,intb){returnab;}// 自动推导返回 intautomultiply(doublea,doubleb){returna*b;}// 返回 double};11. 成员函数类型总览表成员函数类型有 this可改对象可被 const 对象调可虚访问权限普通成员是是否是全部const 成员是const this否除 mutable是是全部静态成员否--否仅静态成员虚函数是取决于声明取决于声明是全部纯虚函数是取决于声明取决于声明是0全部构造函数是构造中是-否全部析构函数是是-应虚基类全部运算符重载通常是取决于设计取决于设计通常不全部12. 面试常考清单12.1 const 成员函数和非 const 成员函数可以构成重载吗答案要点可以。const 成员函数的this指针类型是const T* const非 const 是T* const属于不同的隐式参数类型构成合法的重载。12.2 静态成员函数可以是 const 吗可以是虚函数吗答案要点都不能。静态成员函数没有this指针const修饰没有意义const 修饰的就是 this虚函数的动态绑定也需要通过 this 找到 vptr。编译会报错。12.3 override 和 final 关键字的作用答案要点override显式声明重写基类虚函数编译器会检查签名是否匹配final阻止虚函数被进一步重写或阻止类被继承12.4 为什么需要虚析构函数答案要点通过基类指针删除派生类对象时如果析构函数不虚只会调用基类的析构派生类部分不会析构导致资源泄漏。12.5 哪些运算符必须重载为成员函数答案要点赋值、[]下标、()函数调用、-成员访问必须重载为成员函数。其他运算符可以作为成员或非成员。12.6 为什么有些运算符最好重载为非成员函数答案要点流运算符、的左侧操作数是ostream/istream不能改为自定义类型必须是非成员函数。比较运算符作为非成员函数可以保证对称性允许隐式类型转换在两侧都生效。12.7 右值引用限定的成员函数有什么用途答案要点当对象是右值时即将被销毁可以安全地从对象内部移动资源避免不必要的拷贝。典型场景是从临时对象中获取数据。12.8 inline 成员函数和普通成员函数有什么区别答案要点类内定义的成员函数隐式是inline编译器建议在调用点展开。inline只是对编译器的建议现代编译器有自己的判断逻辑。主要作用是在头文件中定义函数避免多重定义错误。13. 实践准则总结const 正确性不修改对象的成员函数一律标记constoverride重写虚函数时一律加上override虚析构只要类可能被继承析构函数就是虚的静态成员函数用于不依赖对象状态的操作工厂、工具函数运算符重载让类的使用像内置类型但要符合直觉noexcept移动操作、析构函数标记noexcept成员 vs 非成员能用公有接口实现的优先做非成员函数成员函数是对象行为的载体选对函数类型就是给对象赋予了合适的职责。每一种成员函数都有它独特的语义理解和善用这些差异是写出正确、优雅 C 代码的关键。