面向对象基础内容参考:
C++面向对象(一)-CSDN博客
友元函数
友元函数不是类的成员函数,但是被类授予特权,可以访问这个类的 private、protected 成员。
回顾:普通外部函数,只能访问 public;友元是例外。
1. 基础语法
在类里面用
friend声明这个函数,声明写在类里面,定义写在外面。friend 声明放在 public /private/protected 哪个区段都无所谓,权限修饰符对 friend 无效。
#include <iostream> class Student { // 友元声明 friend void PrintStudent(const Student& s); private: int m_id; std::string m_name; public: Student(int id, std::string name) : m_id(id), m_name(std::move(name)) {} }; // 友元函数的定义:不属于 Student,没有 Student:: void PrintStudent(const Student& s) { // ✅ 重点:外部函数,但是可以直接访问 private 成员! std::cout << s.m_id << " " << s.m_name << "\n"; } int main() { Student s{1001, "张三"}; PrintStudent(s); return 0; }⚠️ 重点区分:
void PrintStudent不是成员函数!没有Student::- 只是 Student 类允许它进来偷看私有数据
2. 友元函数的特点
- 不属于类,没有 this 指针不能直接写
m_id,必须传入对象(对象 / 引用),通过.访问成员。- 友元关系单向,不能传递
- A 是 B 的友元 ≠ B 是 A 的友元
- A 是 B 友元,B 是 C 友元 ≠ A 是 C 友元
- 友元声明只是 “授权”,声明在类内,定义在外面
- 一个类可以声明多个友元函数
3. 最经典用途:重载运算符(比如 << 输出)
这是友元函数最高频场景,你后面一定会写:
#include <iostream> class Student { friend std::ostream& operator<<(std::ostream& os, const Student& s); private: int m_id; public: Student(int id) : m_id(id) {} }; std::ostream& operator<<(std::ostream& os, const Student& s) { os << s.m_id; return os; } int main() { Student s{1001}; std::cout << s; return 0; }
operator<<不能做成成员函数(左边是 cout,不是 Student 对象),所以用友元。4. 友元函数 vs 类成员函数对比
项目 友元函数 成员函数 是否属于类 ❌ 不是 ✅ 是 写法 没有 类名::Student::functhis 指针 ❌ 没有 ✅ 有 访问私有成员 ✅ 可以(被授权) ✅ 天然可以 调用方式 func(s)s.func()5. 补充:友元类(顺带了解)
也可以把整个类设为友元:
class A { friend class B; private: int data; }; class B { public: void f(A& a) { a.data = 10; // B的所有成员函数都能访问A私有成员 } };6. 工程上的忠告
友元破坏封装,能不用就尽量不用!封装思想本来就是把私有数据藏起来;友元相当于开了一个后门。优先方案:增加 public get/set 接口;只有运算符重载这类不得不使用的场景才考虑友元。
有个误区很容易混淆:友元函数的直接访问,不是说能直接用对应的名字
友元函数里面,不能直接写
m_id;必须拿着传进来的对象,用对象.成员访问。不像成员函数那样,可以直接写成员名。对比看差别
① 成员函数(有 this 指针)
class Student { public: void Func() { std::cout << m_id; // ✅ 等价 this->m_id,this 是隐式的当前对象指针 } private: int m_id; };成员函数隐含
this,知道 “当前是哪个对象”,所以直接写名字就行。② 友元自由函数(没有 this!!)
class Student { friend void Print(const Student& s); private: int m_id; }; void Print(const Student& s) { // std::cout << m_id; // ❌ 报错!编译器不知道是哪个 Student 的 m_id std::cout << s.m_id; // ✅ 必须指定是 s 这个对象的 m_id }友元只是获得访问权限,但是它是独立的自由函数,没有
this,不知道你说的是哪个对象。一句话澄清你刚才的误解
网上有时候说 “友元可以直接访问私有成员”,这句话是简化说法,它真实意思是:
只要你手里有该类的对象(引用 / 指针),你可以直接用
./->去碰 private,不需要走 get 函数。≠ 不需要对象、直接写成员名字。再举个例子巩固
void PrintTwo(const Student& s1, const Student& s2) { std::cout << s1.m_id << " , " << s2.m_id; }这里有两个对象,
s1.m_id和s2.m_id是两回事,必须区分。极简总结
- 成员函数:自带 this → 直接写成员名
- 友元函数:无 this,是外部函数 →必须传对象参数,通过对象访问成员
- friend 给的是权限,不给 this 指针
普通全局函数
不属于类的函数,就是普通全局函数(或者命名空间里的自由函数),直接写函数名调用就行,不需要依托某个对象。
C++中,全局函数的调用,就和C一样,在头文件中声明给外部使用,或者在调用处使用extern声明,如果不想被外部调用,就直接加static,限制在本文件中。
1. 全局自由函数(最简单)
#include <iostream> // 这是自由函数,不属于任何类 void ShowMessage(const char* msg) { std::cout << msg << std::endl; } int main() { ShowMessage("你好"); // ✅ 直接调用,不需要对象、不需要类名 return 0; }对比成员函数:成员函数必须有对象
s.xxx();自由函数不需要对象。2. 如果这个自由函数是某个类的友元
它依然是自由函数,调用方式不变,只是多了特权:可以访问这个类的私有成员。
#include <iostream> class Student { friend void PrintStudent(const Student& s); private: int m_id; public: Student(int id) : m_id(id) {} }; // 友元,自由函数,不属于Student void PrintStudent(const Student& s) { std::cout << s.m_id << "\n"; } int main() { Student s{1001}; PrintStudent(s); // ✅ 直接传对象进去 return 0; }重点:
PrintStudent不是 Student 的成员;- 调用时是把对象当做参数传进去,函数通过参数访问这个对象;
- 不是
s.PrintStudent(),这个写法是成员函数的调用方式。3. 更好的做法:放到命名空间里(不污染全局)
真实项目很少直接裸写全局函数,一般包在 namespace:
namespace Utils { void ShowInfo(int val) { std::cout << val; } } int main() { Utils::ShowInfo(99); }4. 和类成员函数对比,帮你分清
// 成员函数(属于类) s.Show(); // 自由函数/友元自由函数(不属于类) Show(s);
- 成员函数:主语是对象,对象去执行函数;隐式带 this。
- 自由函数:函数是独立的,对象只是传给函数的一个参数。
5. 一个很关键的思考:什么时候写成成员,什么时候写成自由函数?
简单原则:
- 动作天然属于这个对象本身:成员函数
student.SetId()、student.Show()- 动作是一个外部工具、通用算法,不是对象自己的行为:自由函数(合适的时候可以做友元)典型:
operator<<,cout 输出,不属于 Student 自己的行为。6. 补充坑
友元只是访问权限授权,不改变调用方式、不把它变成成员函数。很多新手误区:
写了 friend,这个函数就变成类里面的了❌ 不是,friend 只是开门,函数本身还是外面独立的。
对比下调用C接口
C++ 里直接调用全局普通 C++ 函数,和调用 C 接口函数(
extern "C"),函数体逻辑本身可以一模一样;核心差别在【名字修饰(name mangling)、链接规则、参数传递、异常、重载】这一套编译链接层面,不是调用语法长得像就一样。很多人混淆两点:
- 这个函数是用 C 语言写的
- 这个函数被声明成
extern "C"(C 链接约定)✅ 重点:
extern "C"控制的是链接名和调用约定,不是函数里面能不能写 C++ 代码(虽然一般不这么干)1. 最大区别:名字修饰(Name Mangling)
① C++ 全局普通函数(默认 C++ 链接)
C++ 编译器会对函数名做名字改编,用来支持重载、类成员、命名空间。
void foo(int a); void foo(double a);编译后在符号表里名字不是
foo,而是类似_Z3fooi、_Z3food这种加密后的名字。👉 所以 C++ 支持函数重载。② extern "C" 函数(C 链接接口)
extern "C" void foo(int a);编译器不做名字修饰,符号名就直接是
foo,和 C 编译器输出的符号名一致。👉不能重载!两个extern "C" void foo(int)会重定义报错。这就是为什么:C 写的动态库 / 静态库给 C++ 调用,必须加
extern "C"声明,否则 C++ 去库里找_Z3fooi,库里只有foo,链接报错(undefined reference)。2. 调用约定(Calling Convention)
这个要看平台,但有通用规律:
- MSVC:C 默认
__cdecl;C++ 全局函数默认一般也是__cdecl,但你可以手动指定__stdcall等- GCC/Clang(Linux):默认都是 System V AMD64 ABI,C 和 C++ 默认调用约定是一样的
⚠️ 调用约定决定:参数怎么压栈、寄存器怎么传参、谁平衡栈。如果两边调用约定不匹配,调用直接栈错乱、随机崩溃。
很多新手误区:
extern "C"一定会改调用约定。❌extern "C"主要改符号名,在大部分 Linux 下不改调用约定;Windows 上部分场景会连带影响。3. 参数、返回值、类型兼容
调用 extern "C" 函数时的限制
extern "C"的函数参数类型不能用 C++ 独有的类型,否则行为危险:
- ❌ 不能传:
std::string、引用int&、类对象(带非平凡构造析构)- ✅ 只能传:基础类型、指针、C 风格结构体(POD 类型)、
char*C 语言没有引用、没有 std::string,C 编译器不认识这些类型。如果你强行在
extern "C"函数参数写std::string,编译可能过,但跨库调用极易出问题。普通 C++ 全局函数:随便用 C++ 所有类型,引用、string、类都没问题。
4. 异常(非常重要的一点)
- C++ 普通全局函数:默认遵循 C++ 异常模型,可以
throw、上层可以try-catch;- extern "C" 接口函数:
C 语言没有异常机制!✅ 规范做法:
extern "C"函数绝对不要抛 C++ 异常!如果在extern "C"函数内部 throw 异常:
- 调用方是 C 代码:直接崩溃,C 没有 catch;
- 调用方是 C++ 代码:虽然 C++ 能 catch,但属于非常不推荐的危险写法;跨 ABI 边界抛异常是 UB(未定义行为)。
最佳实践:C 接口只靠返回值 / 出参返回错误码,不用异常。
5. 链接、跨库场景区别
场景 1:同一个
.cpp文件里,C++ 调用普通 C++ 全局函数
- 同编译单元:直接编译,符号 mangle,支持重载,随便抛异常,所有 C++ 特性可用。
场景 2:C++ 调用一个 C 语言写的库(.so/.dll)C 库里面函数符号是原始名字
foo;C++ 侧必须:extern "C" { void foo(int); }告诉 C++ 编译器:这个符号不要 mangle,直接去库找
foo。场景 3:反过来,C++ 写函数,给 C 代码调用必须写成
extern "C",否则 C 编译器找不到 mangle 后的符号。6. 语法上看起来一样,但底层不一样的例子
// C++全局函数(C++链接) void func(int x) { } // C链接接口 extern "C" void c_func(int x) { } int main() { func(10); // 调用C++函数,符号是 _Z4funci c_func(10); // 调用extern C函数,符号就是 c_func return 0; }你写代码的时候调用写法一模一样,都是
func(xx),但是编译器生成的汇编里,引用的符号名字不一样。简单总结怎么选
- 只在 C++ 内部调用:正常写 C++ 全局函数,享受重载、异常、引用等特性;
- 需要和 C 语言互通、写动态库给别的语言调用(C/Python/Go):用
extern "C"导出 C 接口,只用简单类型、错误码返回,不抛异常。
类的静态成员
在C++中,static有三种用法,其中两种用法跟C里一样的,限制访问范围在本文件,以及函数里修饰变量只初始化一次,在C++中,多了一种用法,那就是限制类内的成员属于类本身,这里重点说明下
分为两大类:静态成员变量、静态成员函数核心一句话:
普通成员归对象;静态成员归类,不属于任何一个对象,整个类只有一份,所有对象共享。
前置区分:
- 普通成员
int m_id:每 new / 定义一个对象,就单独开辟一份内存。s1 有自己的 m_id,s2 有自己的 m_id,互不干扰。- 静态成员
static int m_count:不管你创建多少个 Student 对象,内存里永远只有一份 m_count。1. 静态成员变量 static 成员变量
基础写法
#include <iostream> class Student { public: Student() { ++m_count; // 创建一个对象,计数+1 } private: int m_id; // 普通成员:每个对象独有 static int m_count; // 静态成员【声明】,属于类,共享 }; // ✅ 重点:类外【定义+初始化】(C++17之前必须这样写!) int Student::m_count = 0;访问方式(两种)
// 推荐写法:用类名直接访问,清晰,不需要创建对象 Student::m_count; // 也可以通过对象访问,可读性差,不推荐 Student s; s.m_count;关键规则
- 类里面只是声明,C++17 之前不能在类内初始化
class Student{ static int m_count = 0; // ❌ C++11/14 编译报错! };C++17 新增
static inline可以类内直接初始化,不用类外定义:class Student{ static inline int m_count = 0; // ✅ C++17及以后 };
静态成员变量不随对象销毁而消失对象析构了,静态成员还在,程序结束才释放。
静态成员也遵循
public / private / protected权限上面例子 m_count 在 private,外部直接Student::m_count访问会报错,只能类内、友元、静态成员函数访问。静态成员可以是复杂类型(string、自定义类),不只是 int
2. 静态成员函数 static 成员函数
#include <iostream> class Student { public: Student() { ++m_count; } // 静态成员函数 static int GetCount() { return m_count; } private: static int m_count; }; int Student::m_count = 0;调用:
// 优先:类名:: std::cout << Student::GetCount(); Student s; std::cout << s.GetCount(); // 语法允许,不推荐最重要规则:静态成员函数没有 this 指针!
这是考点核心:
- 普通成员函数:隐式传入
this,代表当前对象,可以直接读取普通成员m_id- static 成员函数:没有 this,它不知道 “现在是哪个对象”
由此引出两条推论:✅ 静态成员函数可以直接访问同类的静态成员(静态变量、静态函数)❌ 静态成员函数不能直接访问普通成员变量 / 普通成员函数
static void Func() { m_id; // ❌ 报错!m_id属于某个对象,不知道是哪个对象 }但是!如果你手动把对象作为参数传进去,就可以访问:
static void ShowId(const Student& s) { std::cout << s.m_id; // ✅ 有明确对象s,就可以 }3. 什么时候用静态成员?
适合:属于这个类型本身,而不属于某个单独实例的数据 / 行为经典场景:
- 对象计数器(上面例子,统计这个类一共创建了多少实例)
- 全局配置、常量、类型共享参数:比如学生统一的学费、统一的班级编号
- 工具函数:和这个类相关,但不需要操作某个特定对象,典型:单例的
GetInstance()class Singleton { public: static Singleton& GetInstance() { static Singleton ins; return ins; } private: Singleton() = default; };❌ 不适合:每个对象自己独有的属性(学号、姓名、年龄,放普通成员)
4. 静态成员 vs 全局自由函数 / 全局变量(很容易混淆)
很多人问:既然全局变量也能计数,为什么还要静态成员?
// 全局方案 int g_student_count = 0;区别:
- 静态成员归属在类里面,名字不会污染全局命名空间,名字是
Student::m_count,不会随便和别的地方重名;- 可以用 private 封装,外部不能随便修改,只能通过静态函数管控;
- 语义清晰:这个变量就是专门给 Student 类使用的。
一句话:静态成员 = 归属于类、受类封装管控的 “类范围全局” 数据 / 函数。
5. 汇总对比表
项目 普通成员 静态成员 归属 属于单个对象 属于整个类,所有对象共享 内存 每个对象单独一份 整个类只有一份 this 指针 ✅ 有 ❌ 没有 访问普通成员 ✅ 直接访问 ❌ 不能直接访问,需要传对象 访问静态成员 ✅ 可以 ✅ 可以 调用 对象.xxx () 优先 类名::xxx () 6. 高频坑小结
- C++17 前静态变量必须在类外定义初始化;
- static 成员函数没有 this,不能直接碰非 static 成员;
- static 只是共享,依然受访问控制(private 静态成员外面访问不了);
- 静态成员独立于对象,没有对象,也可以调用静态成员函数、读取静态变量。
类静态函数怎么写实现?
静态成员函数属于类,实现的时候,要加
类名::;但是函数前面不要再写 static!static 只在类内的声明处写,外面定义实现时不能重复写 static,写了会报错。
完整示例
#include <iostream> class Student { public: // 声明:这里写 static static int GetCount(); static void SetCount(int val); private: static int m_count; }; // ====== 实现部分 ====== // ❌ 错误:前面再加 static // static int Student::GetCount() {} // ✅ 正确:不加 static,带上 Student:: int Student::GetCount() { return m_count; // 可以直接访问同类静态成员变量 } void Student::SetCount(int val) { m_count = val; } // 静态成员变量的定义初始化(C++17之前) int Student::m_count = 0; int main() { // 调用,推荐类名:: Student::SetCount(10); std::cout << Student::GetCount(); return 0; }规则梳理
- 类内声明:写 static
static int GetCount();
- 类外实现:去掉 static,必须加
类名::int Student::GetCount() { }理由:
static是声明修饰符,用来告诉编译器 “这是静态成员”;链接层面它不是函数类型的一部分,定义处重复 static 属于语法错误。
- 静态成员函数没有 this所以在函数里面,不能直接访问非静态成员变量 / 非静态成员函数。如果要操作普通成员,必须把对象传进来当参数:
// ✅ 类外定义 + 初始化 int Student::m_count = 0; 啥意思,这是说在使用的地方执行,还是在类对应的cpp文件里执行呢
先说核心答案:这一行代码,写在这个类对应的 .cpp 文件里面,不是写在头文件,也不是写在 main、不是写在使用对象的地方。它不是运行时调用才执行,是程序启动阶段就完成初始化。
// Student.cpp #include "Student.h" int Student::m_count = 0;这行代码是什么性质?
int Student::m_count = 0;这是变量定义:
- 告诉编译器:给
Student的静态成员m_count分配一块内存;- 并且把这块内存初始化为 0;
Student::用来指明:这个变量属于 Student 类里面的那个静态成员。类里面写的
static int m_count;仅仅只是声明:“有这么一个变量”,不分配内存。
this 指针
在 C++ 中,this指针是一个特殊的指针,它指向当前对象的实例。
在 C++ 中,每一个对象都能通过this指针来访问自己的地址。
this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
友元函数没有this指针,因为友元不是类的成员,只有成员函数才有this指针。
下面的实例有助于更好地理解 this 指针的概念:
#include <iostream> class MyClass { private: int value; public: void setValue(int value) { this->value = value; } void printValue() { std::cout << "Value: " << this->value << std::endl; } }; int main() { MyClass obj; obj.setValue(42); obj.printValue(); return 0; }实例解析:
以上实例中,我们定义了一个名为 MyClass 的类,它有一个私有成员变量 value。
类中的 setValue() 函数用于设置 value的 值,而 printValue() 函数用于打印 value 的值。
在 setValue() 函数中,我们使用 this 指针来引用当前对象的成员变量 value,并将传入的值赋给它,这样可以明确地告诉编译器我们想要访问当前对象的成员变量,而不是函数参数或局部变量。
在 printValue() 函数中,我们同样使用 this 指针来引用当前对象的成员变量 value,并将其打印出来。
在 main() 函数中,我们创建了一个 MyClass 的对象 obj,然后使用 setValue() 函数设置 value 的值为 42,并通过 printValue() 函数打印出来。
通过使用 this 指针,我们可以在成员函数中访问当前对象的成员变量,即使它们与函数参数或局部变量同名,这样可以避免命名冲突,并确保我们访问的是正确的变量。
指向类的指针
类比结构体指针。
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
在 C++ 中,指向类的指针指向一个类的对象,与普通的指针相似,指向类的指针可以用于访问对象的成员变量和成员函数。
声明和初始化指向类的指针
#include <iostream> class MyClass { public: int data; void display() { std::cout << "Data: " << data << std::endl; } }; int main() { // 创建类对象 MyClass obj; obj.data = 42; // 声明和初始化指向类的指针 MyClass *ptr = &obj; // 通过指针访问成员变量 std::cout << "Data via pointer: " << ptr->data << std::endl; // 通过指针调用成员函数 ptr->display(); return 0; }当上面的代码被编译和执行时,它会产生下列结果:
Data via pointer: 42 Data: 42动态分配内存
指向类的指针还可以用于动态分配内存,创建类的对象:
#include <iostream> class MyClass { public: int data; void display() { std::cout << "Data: " << data << std::endl; } }; int main() { // 动态分配内存创建类对象 MyClass *ptr = new MyClass; ptr->data = 42; // 通过指针调用成员函数 ptr->display(); // 释放动态分配的内存 delete ptr; return 0; }指向类的指针可以作为函数参数传递
#include <iostream> class MyClass { public: int data; void display() { std::cout << "Data: " << data << std::endl; } }; // 函数接受指向类的指针作为参数 void processObject(MyClass *ptr) { ptr->display(); } int main() { MyClass obj; obj.data = 42; // 将指向类的指针传递给函数 processObject(&obj); return 0; }其实就和结构体指针用法类似。
特点
内存分配:在堆(heap)上分配,需手动管理内存。
生命周期:直到显式调用
delete时才会销毁。优点:
堆空间大(受系统内存限制),适合大对象。
生命周期可灵活控制。
缺点:
必须手动
delete,否则内存泄漏。忘记释放会导致资源泄露,多次释放会崩溃。
其实new的时候,就相当于c里面给结构体指针malloc了一块地址空间。
C++定义类指针的时候会为类分配地址空间吗
在 C++ 中,定义类指针时不会自动为类对象分配内存空间。指针本身只是一个存储地址的变量,它需要明确指向一个有效的对象地址才能安全使用。以下是详细分析:
指针定义与内存分配的关系
(1) 仅定义指针(未初始化)
MyClass* ptr; // 只分配了指针本身的内存(通常4/8字节),未分配类对象的内存指针变量
ptr: 编译器会为指针分配内存(32位系统4字节,64位系统8字节),用于存储地址。类对象
MyClass:未被分配内存,此时ptr是“野指针”(指向随机地址),访问会导致未定义行为(崩溃或数据错误)。(2) 初始化指针后
MyClass* ptr = new MyClass(); // 动态分配类对象内存,ptr存储其地址
new关键字: 在堆(Heap)上为MyClass对象分配内存,并调用构造函数。
ptr的作用: 存储该对象的地址,通过ptr->member访问对象成员。内存分配方式对比
操作 类对象内存分配位置 生命周期管理 示例 动态分配(new) 堆(Heap) 需手动 delete MyClass* p = new MyClass; 栈对象(自动变量) 栈(Stack) 作用域结束自动销毁 MyClass obj; 智能指针 堆 自动管理 auto p = make_shared<MyClass>(); 常见错误示例
(1) 野指针(未初始化)
MyClass* ptr; ptr->doSomething(); // 崩溃!ptr未指向有效对象(2) 内存泄漏(未释放)
MyClass* ptr = new MyClass();// 忘记 delete,对象内存永远泄漏(3) 悬空指针(Dangling Pointer)
MyClass* ptr = new MyClass();delete ptr; // 释放内存 ptr->doSomething(); // 危险!ptr仍存储旧地址安全实践建议
(1) 初始化时分配对象
MyClass* ptr = new MyClass(); // 分配内存 + 构造 // 使用后... delete ptr; // 释放内存 + 析构(2) 使用智能指针(C++11起推荐)
include <memory> std::unique_ptr<MyClass ptr = std::make_unique<MyClass(); // 自动释放(3) 优先使用栈对象(无需手动管理)
MyClass obj; // 自动分配在栈上,作用域结束销毁底层原理
指针的本质: 指针变量存储的是内存地址(如
0x7ffd1234),其大小与系统架构相关(与指向的类型无关)。对象内存分配:
new操作符调用operator new分配内存,然后调用构造函数。对象的内存大小由类成员变量和编译器填充决定(可通过
sizeof(MyClass)获取)。