目录
二、类成员变量:声明与定义的边界剖析——从私有成员到内存分配(先了解,等学到了后面再回顾)
前言:面向过程与面向对象的初步认识
- C语言是面向过程的编程语言,关注的是过程,通过分析问题的解决步骤,用函数调用来逐步解决问题。
- C++是基于面向对象的编程语言,关注的是对象,将问题分解为不同的对象,通过对象之间的交互来完成功能。
一、类的定义
1、类的引入
在C++中,结构体(struct)不仅可以定义变量,还可以定义函数:
struct Test
{
// 成员变量
int a;
double b;
// 成员函数
int Add(int x, int y)
{
return x + y;
}
};
但在C++中更常用class
关键字来定义类。
2、类的定义
class className
{
// 类体:由成员变量和成员函数组成
}; // 注意后面的分号
class
是定义类的关键字className
是类名类体包含成员变量(属性)和成员函数(方法)
类定义结束时必须加上分号
3、成员变量命名规范
为了区分成员变量和局部变量,通常会对成员变量添加特殊标识:
class Date
{
private:
int _year; // 前面加下划线
int m_month; // m开头
int day_; // 后面加下划线
};
注意:这只是编程惯例,并非C++强制要求,具体命名规范可能因公司而异。
4、类的两种定义方式
声明和定义全部放在类体中
class Person
{
public:
// 显示基本信息
void ShowInfo()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
private:
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
特点:
代码紧凑,适合简单类
在类中定义的成员函数默认可能被当作内联函数处理
声明和定义分离
头文件(person.h):
class Person
{
public:
// 显示基本信息
void ShowInfo();
private:
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
源文件(person.cpp):
#include "person.h"
#include <iostream>
// 显示基本信息
void Person::ShowInfo()
{
std::cout << _name << "__" << _sex << "__" << _age << std::endl;
}
特点:
提高代码可读性和可维护性
减少头文件的依赖
避免潜在的重复定义问题
更符合大型项目的组织规范
5、类使用示例(先了解,后面详讲)
int main()
{
// Stack类使用示例
Stack st;
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
// Date类使用示例
Date d;
d.Init(2024, 3, 31);
return 0;
}
6、最佳实践建议
对于简单类,可以在类体中直接定义成员函数
对于复杂类,推荐使用声明和定义分离的方式
保持成员变量命名一致性
优先使用
class
而非struct
来定义类注意资源管理(如示例中的malloc/free)
二、类成员变量:声明与定义的边界剖析——从私有成员到内存分配(先了解,等学到了后面再回顾)
1、声明 vs 定义的本质区别
声明(Declaration):向编译器介绍标识符(变量/函数)的存在和类型信息,但不分配内存
int x; // 声明(也是定义,特殊情况) extern int y; // 纯声明
定义(Definition):完成声明的同时分配存储空间
int x = 10; // 定义
2、类成员变量的特殊情况
在类定义中的成员变量(无论private/public/protected
)都是声明:
class Example {
private:
int a; // 声明(未分配内存)
double b; // 声明
};
内存分配时机:只有当创建类的具体对象时,这些成员变量才会被真正定义(分配内存)
Example obj; // 此时obj.a和obj.b才被定义(分配内存)
3、需要特别注意的情况
静态成员变量:类内声明,类外必须单独定义(因为需要独立存储)
class Example { private: static int count; // 声明 }; int Example::count = 0; // 必须类外定义
成员变量的初始化:C++11后支持类内直接初始化(仍是声明)
class Example { private: int a = 10; // 声明带默认值(C++11特性) };
4、为什么这样设计?
抽象性:类定义只是"蓝图",不占用实际内存
灵活性:允许不同编译单元包含相同的类定义
效率:避免多次定义导致的存储冲突
关键结论
- 类定义中的成员变量(包括私有成员)都是声明
- 实际内存分配发生在实例化对象时
- 静态成员变量需要额外在类外定义
- 类内初始化(C++11)是语法糖,不改变声明本质
三、C++类的访问控制与封装机制
1、封装的概念与实现
C++通过类(class)机制实现面向对象编程中的封装特性。封装是将对象的属性(数据成员)和方法(成员函数)有机结合在一起,通过访问权限控制,有选择性地对外提供接口,同时隐藏内部实现细节。
2、访问限定符
作用与意义
C++通过访问限定符实现面向对象编程中的封装特性,这是将数据与操作数据的方法有机结合的重要机制。访问限定符允许开发者精确控制类成员的可见性,从而:
保护内部数据不被随意修改
提供清晰的使用接口
隐藏实现细节,降低耦合度
C++提供了三种访问限定符来控制类成员的可见性:
public(公有成员):公有成员可以在类外直接被访问,构成类的对外接口。
protected(受保护成员):受保护成员只能在类内部和派生类中访问,类外不可直接访问。示例:派生类中可访问基类protected成员
private(私有成员):私有成员只能在类内部访问,类外部和派生类都不可直接访问(默认访问级别)。
重要区别
protected和private在当前类中表现相同,区别仅体现在继承关系中(派生类可访问基类protected成员,但不能访问private成员)
访问控制规则
访问权限从访问限定符出现的位置开始生效,直到遇到下一个访问限定符或类定义结束(即遇到
}
)class的默认访问权限是private,struct的默认访问权限是public(为了兼容C语言的结构体)
重要说明:
访问限定符仅在编译阶段起作用,当程序运行时,所有成员在内存中的布局没有区别,访问控制纯粹是编译器级别的保护机制。
最佳实践建议:
成员变量:通常设为private/protected
防止外部直接修改
可通过公有成员函数控制访问
示例:
class Person { private: std::string name; // 私有成员变量 public: void setName(const std::string& n) { name = n; } std::string getName() const { return name; } };
成员函数:
对外接口设为public
内部辅助函数设为private
示例:
class Calculator { public: double add(double a, double b) { return a + b; } private: void logOperation(const std::string& op) { /* 记录操作日志 */ } };
struct的特殊用法:
当需要POD(Plain Old Data)类型时使用struct
需要完全公开成员时使用struct
示例:
struct Point { // 默认为public int x; int y; };
3、struct与class的区别
在C++中,struct也可以用于定义类。虽然C++保留了C语言中struct的用法,但将其功能进行了扩展,使其升级为完整的类。最显著的变化是struct现在可以包含成员函数。不过在实际开发中,我们通常更推荐使用class来定义类。
面试常见问题:C++中struct和class有何区别?
标准答案:
兼容性区别:struct保持了对C语言结构体的兼容,可以像C结构体一样使用。C++完全兼容C的struct用法、C++的struct可以包含成员函数
默认访问权限:struct成员默认是public的,class成员默认是private的
继承默认权限:struct继承默认是public继承,class继承默认是private继承
模板参数:class可作为模板参数关键字,struct不能
除此之外,struct和class在功能上完全等价,都可以用于定义类,包含成员函数、实现继承等面向对象特性。
// C风格结构体
typedef struct ListNodeC
{
struct ListNodeC* next;
int val;
} LTNode;
// C++风格结构体
struct ListNodeCPP
{
void Init(int x)
{
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
};
4、封装的本质与意义
封装本质上是一种管理机制,其核心思想是:
数据保护:隐藏对象的内部状态和实现细节
接口暴露:仅对外提供必要的访问和操作接口
使用约束:通过接口限制对数据的随意修改
在软件开发中,封装带来的好处包括:降低耦合度、提高安全性、便于修改和维护、简化接口使用
良好的封装设计是高质量面向对象程序的基础,它能有效隔离变化,提高代码的稳定性和可维护性。
四、类的作用域详解
1、类作用域的基本概念
类定义了一个新的作用域,类的所有成员(包括数据成员和成员函数)都在这个类的作用域中。当在类体外定义成员时,需要使用::
作用域解析符来指明成员属于哪个类域。
2、示例说明
基本示例:Person类
class Person {
public:
// 声明成员函数
void ShowInfo();
private:
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
// 在类外定义成员函数时需要指定类域
void Person::ShowInfo() {
cout << _name << "-" << _sex << "-" << _age << endl;
}
深入理解:Stack类
#include <iostream>
using namespace std;
class Stack {
public:
// 成员函数声明
void Init(int n = 4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};
// 成员函数定义,必须指定类域
void Stack::Init(int n) {
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array) {
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
int main() {
Stack st;
st.Init(); // 使用默认参数初始化
return 0;
}
3、类域的重要性
名称查找规则:类域影响编译器的查找规则。当在类外定义成员函数时:
如果不指定类域,编译器会把函数当作全局函数处理
指定类域后,编译器知道这是成员函数,会在类域中查找其他成员
避免命名冲突:类作用域可以避免成员名称与全局名称的冲突
组织代码:类作用域帮助组织代码,使成员函数的实现与声明分离,提高代码可读性
4、常见错误
如果在类外定义成员函数时忘记指定类域,会导致编译错误:
// 错误写法:缺少Stack::
void Init(int n) { /*...*/ } // 编译器会认为这是全局函数
编译器会报错,因为在这个"全局函数"中访问的array
、capacity
等成员无法找到定义。