C++面向对象(二) 面向对象基础内容参考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_idthis 是隐式的当前对象指针 } private: int m_id; };成员函数隐含this知道 “当前是哪个对象”所以直接写名字就行。② 友元自由函数没有 thisclass 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. 更好的做法放到命名空间里不污染全局真实项目很少直接裸写全局函数一般包在 namespacenamespace Utils { void ShowInfo(int val) { std::cout val; } } int main() { Utils::ShowInfo(99); }4. 和类成员函数对比帮你分清// 成员函数属于类 s.Show(); // 自由函数/友元自由函数不属于类 Show(s);成员函数主语是对象对象去执行函数隐式带 this。自由函数函数是独立的对象只是传给函数的一个参数。5. 一个很关键的思考什么时候写成成员什么时候写成自由函数简单原则动作天然属于这个对象本身成员函数student.SetId()、student.Show()动作是一个外部工具、通用算法不是对象自己的行为自由函数合适的时候可以做友元典型operatorcout 输出不属于 Student 自己的行为。6. 补充坑友元只是访问权限授权不改变调用方式、不把它变成成员函数。很多新手误区写了 friend这个函数就变成类里面的了❌ 不是friend 只是开门函数本身还是外面独立的。对比下调用C接口C 里直接调用全局普通 C 函数和调用 C 接口函数extern C函数体逻辑本身可以一模一样核心差别在【名字修饰name mangling、链接规则、参数传递、异常、重载】这一套编译链接层面不是调用语法长得像就一样。很多人混淆两点这个函数是用 C 语言写的这个函数被声明成extern CC 链接约定✅ 重点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这个要看平台但有通用规律MSVCC 默认__cdeclC 全局函数默认一般也是__cdecl但你可以手动指定__stdcall等GCC/ClangLinux默认都是 System V AMD64 ABIC 和 C 默认调用约定是一样的⚠️ 调用约定决定参数怎么压栈、寄存器怎么传参、谁平衡栈。如果两边调用约定不匹配调用直接栈错乱、随机崩溃。很多新手误区extern C一定会改调用约定。❌extern C主要改符号名在大部分 Linux 下不改调用约定Windows 上部分场景会连带影响。3. 参数、返回值、类型兼容调用 extern C 函数时的限制extern C的函数参数类型不能用 C 独有的类型否则行为危险❌ 不能传std::string、引用int、类对象带非平凡构造析构✅ 只能传基础类型、指针、C 风格结构体POD 类型、char*C 语言没有引用、没有 std::stringC 编译器不认识这些类型。如果你强行在extern C函数参数写std::string编译可能过但跨库调用极易出问题。普通 C 全局函数随便用 C 所有类型引用、string、类都没问题。4. 异常非常重要的一点C 普通全局函数默认遵循 C 异常模型可以throw、上层可以try-catchextern C 接口函数C 语言没有异常机制✅ 规范做法extern C函数绝对不要抛 C 异常如果在extern C函数内部 throw 异常调用方是 C 代码直接崩溃C 没有 catch调用方是 C 代码虽然 C 能 catch但属于非常不推荐的危险写法跨 ABI 边界抛异常是 UB未定义行为。最佳实践C 接口只靠返回值 / 出参返回错误码不用异常。5. 链接、跨库场景区别场景 1同一个.cpp文件里C 调用普通 C 全局函数同编译单元直接编译符号 mangle支持重载随便抛异常所有 C 特性可用。场景 2C 调用一个 C 语言写的库.so/.dllC 库里面函数符号是原始名字fooC 侧必须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_ids2 有自己的 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; // 静态成员【声明】属于类共享 }; // ✅ 重点类外【定义初始化】C17之前必须这样写 int Student::m_count 0;访问方式两种// 推荐写法用类名直接访问清晰不需要创建对象 Student::m_count; // 也可以通过对象访问可读性差不推荐 Student s; s.m_count;关键规则类里面只是声明C17 之前不能在类内初始化class Student{ static int m_count 0; // ❌ C11/14 编译报错 };C17 新增static inline可以类内直接初始化不用类外定义class Student{ static inline int m_count 0; // ✅ C17及以后 };静态成员变量不随对象销毁而消失对象析构了静态成员还在程序结束才释放。静态成员也遵循public / private / protected权限上面例子 m_count 在 private外部直接Student::m_count访问会报错只能类内、友元、静态成员函数访问。静态成员可以是复杂类型string、自定义类不只是 int2. 静态成员函数 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_idstatic 成员函数没有 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. 高频坑小结C17 前静态变量必须在类外定义初始化static 成员函数没有 this不能直接碰非 static 成员static 只是共享依然受访问控制private 静态成员外面访问不了静态成员独立于对象没有对象也可以调用静态成员函数、读取静态变量。类静态函数怎么写实现静态成员函数属于类实现的时候要加类名::但是函数前面不要再写 staticstatic 只在类内的声明处写外面定义实现时不能重复写 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; } // 静态成员变量的定义初始化C17之前 int Student::m_count 0; int main() { // 调用推荐类名:: Student::SetCount(10); std::cout Student::GetCount(); return 0; }规则梳理类内声明写 staticstatic 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分配一块内存并且把这块内存初始化为 0Student::用来指明这个变量属于 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需手动 deleteMyClass* p new MyClass;栈对象自动变量栈Stack作用域结束自动销毁MyClass obj;智能指针堆自动管理auto p make_sharedMyClass();常见错误示例(1) 野指针未初始化MyClass* ptr; ptr-doSomething(); // 崩溃ptr未指向有效对象(2) 内存泄漏未释放MyClass* ptr new MyClass();// 忘记 delete对象内存永远泄漏(3) 悬空指针Dangling PointerMyClass* ptr new MyClass();delete ptr; // 释放内存 ptr-doSomething(); // 危险ptr仍存储旧地址安全实践建议(1) 初始化时分配对象MyClass* ptr new MyClass(); // 分配内存 构造 // 使用后... delete ptr; // 释放内存 析构(2) 使用智能指针C11起推荐include memory std::unique_ptrMyClass ptr std::make_uniqueMyClass(); // 自动释放(3) 优先使用栈对象无需手动管理MyClass obj; // 自动分配在栈上作用域结束销毁底层原理指针的本质 指针变量存储的是内存地址如0x7ffd1234其大小与系统架构相关与指向的类型无关。对象内存分配new操作符调用operator new分配内存然后调用构造函数。对象的内存大小由类成员变量和编译器填充决定可通过sizeof(MyClass)获取。