☰
C++示例讲解friend static const关键字的用法
2026/10/21 5:49:50 网站建设 项目流程

前言


friend、static、const这三个关键字经常被放在一起讲,是因为它们都在回答同一个问题:谁可以访问什么,以及什么时候可以修改。friend决定「非成员能不能碰私有成员」,static决定「这个成员属于对象还是属于类」,const决定「这个接口承诺不修改什么」。三者各自独立,却在类的设计里频繁交叉:一个const成员函数里能不能改缓存?一个static成员函数能不能访问普通成员?友元函数要不要加const引用参数?这些问题都要求把三者的语义分清楚。


常见的误解有:



  • 以为friend是双向的,或者会传递给派生类;

  • 以为static成员函数里可以用this;

  • 以为在成员函数后面写const只是「文档性质」,编译器不会检查;

  • 以为const变量一定是编译期常量,因而把它用在需要常量表达式的地方。


这些理解偏差在编译期通常就会暴露,但错误信息往往很长,容易被误读成「编译器坏了」。本文按 C++17 逐条讲清三个关键字的语义,再用一个账户类把它们串起来。文中代码未在本机编译或运行,语义按标准推演,读者应以自己的工具链为准。


一、friend:有选择地开放私有访问


类的私有成员默认只对类的成员函数和友元开放。friend声明的作用,是把某个函数或某个类加入这个白名单。


1.1 友元函数


最典型的用途是重载流输出运算符。operator<<的左操作数是std::ostream,不可能是我们自己的类,所以它只能写成非成员函数;但它又需要读私有成员,于是声明为友元:


// 适用于 C++17
#include <ostream>

class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}

friend std::ostream& operator<<(std::ostream& os, const Point& p);

private:
int x_;
int y_;
};

std::ostream& operator<<(std::ostream& os, const Point& p) {
os << '(' << p.x_ << ", " << p.y_ << ')';
return os;
}

几个要点:



  • 类内的friend声明只是「授予访问权」,不是函数声明意义上的成员。真正的声明在命名空间作用域,定义时不能写friend。

  • 若在类内直接给出函数体(友元定义),该函数只能通过实参依赖查找(argument-dependent lookup, ADL)找到,普通的名字查找看不到它。这在写模板类时容易踩坑。

  • 友元函数不是成员函数,没有this,因此不存在「友元函数能不能是 const」的问题;它接收的const Point&才决定了能否修改对象。

  • 友元关系不互惠:A 声明 B 是友元,只表示 B 能访问 A 的私有成员,不代表 A 能访问 B 的。

  • 友元关系不传递:B 是 A 的友元、C 是 B 的友元,不意味着 C 能访问 A。

  • 友元关系不被继承:基类的友元不会自动成为派生类的友元,反之亦然。


1.2 友元类与友元成员函数


friend class Auditor;表示Auditor的所有成员函数都能访问本类的私有成员。范围要慎重:这等于把整个类加入白名单。更细粒度的做法是只友元某一个成员函数,但语法更绕,且要求该成员函数所在类已经声明:


// 适用于 C++17
class Account;

class Auditor {
public:
static double peek(const Account& a);
};

class Account {
public:
explicit Account(double b) : balance_(b) {}
friend double Auditor::peek(const Account& a); // 友元成员函数
private:
double balance_ = 0.0;
};

double Auditor::peek(const Account& a) {
return a.balance_;
}

顺序很关键:Auditor必须先完整定义,才能用Auditor::peek这种限定名。若只是friend class Auditor;,则可以先声明class Auditor;再在Account里授予友元。


二、static:属于类,而不是属于某个对象


static在不同位置含义不同,必须分开看。


2.1 静态数据成员


静态数据成员在类的所有对象之间共享一份存储,它不在对象布局里,sizeof一个对象不包含它。C++17 之前,类内只能声明,必须在类外定义一次:


// 适用于 C++17
class Widget {
public:
Widget() { ++alive_; }
~Widget() { --alive_; }
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;

static int alive() { return alive_; }

private:
static int alive_; // 声明
};

int Widget::alive_ = 0; // 定义,只能出现在一个翻译单元

如果这个头文件被多个.cpp包含,而定义写在头文件里,就会违反单一定义规则(one definition rule, ODR),链接期报重复定义。C++17 引入了内联变量,可以直接在类内写inline static int alive_ = 0;,省去类外定义。写库代码时这很方便,但要注意它要求 C++17。


2.2 静态成员函数


静态成员函数没有this指针,因此:



  • 不能声明为const、volatile或引用限定;

  • 不能直接访问非静态成员,因为没有对象可依附;

  • 可以通过Widget::alive()调用,也可以通过对象调用(但前者更清楚);

  • 可以访问私有静态成员。


一个常见设计是把工厂方法或工具函数写成静态成员。它和命名空间里的自由函数相比,优势主要是名字归类清晰、能访问私有成员。


2.3 函数内的静态局部变量


函数内的static变量在第一次执行到声明处时初始化,之后跨调用保持值。C++11 起,其初始化是线程安全的(编译器通常用一次性守卫实现),这使它成为实现懒初始化单例的常见手段:


// 适用于 C++17
Logger& logger() {
static Logger instance; // 首次调用时构造,C++11 起线程安全
return instance;
}

但「初始化线程安全」不等于「之后的操作线程安全」。如果Logger的方法会被多线程同时调用,内部仍需同步。


2.4 命名空间作用域的 static


在命名空间作用域写static int counter;,含义是内部链接(internal linkage),该名字只在当前翻译单元可见。现代 C++ 更推荐用匿名命名空间表达同样的意图,语义更清晰,也不与类内static混淆。


三、const:接口上的承诺


3.1 const 对象与 const 成员函数


在成员函数参数列表后写const,表示该函数不会修改对象的非mutable成员,也不会调用其他非 const 成员函数。这个承诺由编译器检查,不是注释。


// 适用于 C++17
class Box {
public:
int width() const { return width_; } // 可被 const 对象调用
void set_width(int w) { width_ = w; } // 不可被 const 对象调用

private:
int width_ = 0;
};

对一个const Box b;,只能调用width(),不能调用set_width()。若在width()里写width_ = 1;,编译会直接报错。这正是 const 正确性(const-correctness)的价值:把「只读」写进类型,让误用在编译期被发现。


3.2 mutable 的用途


mutable允许在 const 成员函数里修改某个成员,用于「逻辑上不改变对象可观察状态」的缓存、互斥量、惰性求值标记等:


// 适用于 C++17
class Sensor {
public:
int value() const {
if (!cached_) {
cached_value_ = read_hardware();
cached_ = true;
}
return cached_value_;
}

private:
int read_hardware() const { return 42; } // 示意

mutable int cached_value_ = 0;
mutable bool cached_ = false;
};

mutable不是「绕过 const 的后门」,而是一个需要谨慎使用的设计选择。上面的缓存在多线程下并不安全,因为cached_与cached_value_的读写没有同步;若对象会被多个线程共享,需要额外加锁,而std::mutex本身通常也声明为mutable,才能在 const 成员函数里加锁。


3.3 指针与引用的 const 位置


const出现在*左边还是右边,含义完全不同:




写法含义能否改指向能否改所指的值



const int* p指向 const int 的指针能不能

int* const pconst 指针,指向 int不能能

const int* const p两者都 const不能不能

const int& r绑定到 const 的引用不适用不能



读法是从右往左:int* const是「const 的指针,指向 int」;const int*是「指针,指向 const int」。引用本身一旦绑定不可更换,所以只有「引用到 const」这一种写法。


3.4 const 与重载


成员函数可以仅凭末尾的const重载,编译器根据对象是否为 const 选择版本:


// 适用于 C++17
#include <cstddef>
#include <vector>

class Buffer {
public:
explicit Buffer(std::size_t n) : data_(n, '\0') {}

char& at(std::size_t i) { return data_[i]; }
const char& at(std::size_t i) const { return data_[i]; }

private:
std::vector<char> data_;
};

非 const 对象调用非 const 版本,返回可写引用;const 对象调用 const 版本,返回只读引用。若只提供 const 版本,非 const 对象也只能拿到只读引用,会限制可用性。


完整示例:三个关键字一起用


下面的账户类把静态计数、const 成员函数、友元输出和友元类放在一个例子里。为让计数准确,拷贝构造被显式删除,避免「复制对象却没有增加计数」。


// 适用于 C++17
#include <iostream>
#include <string>
#include <utility>

class Auditor;

class Account {
public:
Account(std::string owner, double balance)
: owner_(std::move(owner)), balance_(balance) {
++count_;
}

~Account() { --count_; }

Account(const Account&) = delete;
Account& operator=(const Account&) = delete;

const std::string& owner() const { return owner_; }
double balance() const { return balance_; }

void deposit(double amount) {
if (amount > 0.0) {
balance_ += amount;
}
}

bool withdraw(double amount) {
if (amount <= 0.0 || amount > balance_) {
return false;
}
balance_ -= amount;
return true;
}

static int count() { return count_; }

friend std::ostream& operator<<(std::ostream& os, const Account& a);
friend class Auditor;

private:
std::string owner_;
double balance_ = 0.0;
static int count_; // 声明
};

int Account::count_ = 0; // 定义

std::ostream& operator<<(std::ostream& os, const Account& a) {
os << a.owner_ << ": " << a.balance_;
return os;
}

class Auditor {
public:
static double total(const Account& a) {
return a.balance_; // 友元类可访问私有成员
}
};

逐行核对:



  • owner()返回const std::string&,避免拷贝,同时承诺调用者不能通过返回值修改内部字符串。

  • count_是私有静态成员,构造时自增、析构时自减;拷贝被删除,所以不会出现「复制后计数不变」的问题。

  • operator<<在类内声明为友元,类外定义时不写friend,签名与声明一致。

  • Auditor::total能读a.balance_,是因为friend class Auditor;授权了整个类。

  • Account没有提供默认构造,创建对象必须传两个参数,避免出现「空账户」。


static int count_的定义必须恰好出现一次。若这段代码放在头文件里被多个翻译单元包含,应改成 C++17 的内联静态成员:


// 适用于 C++17
inline static int count_ = 0; // 类内定义,无需类外再写一遍

常见坑点



  1. ❌ 在类外定义友元函数时又写一遍friend。


✅friend只出现在类内的声明中;类外定义按普通函数写,签名保持一致即可。



  1. ❌ 以为友元关系会传给派生类,于是让派生类直接访问基类友元类的私有成员。


✅ 友元不被继承、不传递;需要访问就在对应类里单独声明。



  1. ❌ 把静态数据成员的定义写进头文件,多个.cpp包含后链接报重复定义。


✅ 头文件里只放声明,定义放到一个.cpp;或者用 C++17 的inline static在类内定义。



  1. ❌ 在static成员函数里直接写balance_ += 1;或使用this。


✅ 静态成员函数没有this;要么传入对象引用,要么只访问静态成员。



  1. ❌ 在const成员函数里修改普通成员,靠「编译器也许不管」蒙混。


✅ 不允许就是不允许;确实需要缓存时把成员声明为mutable,并想清楚线程安全。



  1. ❌ 把const int* p和int* const p当成一回事。


✅ 从右往左读:前者不能改所指的值,后者不能改指向;两者都 const 就写const int* const。



  1. ❌ 认为const变量一定能用在case标签或数组长度里。


✅ 只有常量表达式才行;需要编译期常量用constexpr,const只保证运行期不可修改。



  1. ❌ 在const成员函数里返回内部成员的裸指针或非 const 引用,把只读承诺泄漏出去。


✅ 返回const引用或值;若必须返回可写引用,就不要把该函数声明为const。


总结




关键字核心作用常见位置关键限制



friend授权非成员访问私有成员类内声明不互惠、不传递、不继承

static成员属于类而非对象数据成员、成员函数、局部变量静态函数无this;数据成员需定义

const承诺不修改可观察状态变量、参数、成员函数、指针只约束被标注的层级;mutable可例外



三个关键字分别对应「访问权」「归属」「可变性」三个维度。写类时先想清楚哪些状态属于对象、哪些属于类,哪些接口应当只读,再把友元收窄到真正需要的函数或类。这样得到的接口比「到处 friend、到处 static、到处 const」更容易维护,也更不容易在重构时出错。本文示例按 C++17 语义书写,未编译运行,落地前请结合自己的编译器和测试核对。




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

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

立即咨询