【C++】入门基础2

C++基础2:引用 · inline · nullptr · 类和对象初探

目录


第一章 引用(Reference)

引入

C 语言里让函数修改外部变量只能靠指针,繁琐且易错。C++ 提供了更简洁安全的方式——引用

提示: “引用在实践中主要用于两个地方:传参和做返回值。其余理解都是为这两个场景服务的。”


1.1 引用是什么——别名哲学

引用(Reference):给已存在变量取别名,两个名字指向同一块内存,语法层面不开辟新空间

int a = 10;
int& b = a;     // b 是 a 的别名,&b == &a
int& c = b;     // 对别名再取别名,c 也是 a 的别名

注意: 定义时 int& 中的 &引用声明符,不是取地址运算符。


1.2 引用的三大语法特性

1.2.1 必须初始化
int& ra;         // × 编译报错:引用必须初始化
int& ra = a;     // √ 定义即绑定
1.2.2 不能改变指向(一旦绑定,终身不变)

这是 C++ 引用与 Java 引用最本质的区别

int a = 10, d = 20;
int& b = a;      // b 是 a 的别名
b = d;           // **注意:** 这是赋值,不是改变指向!
                 // 等价于 a = d,a 值变为 20
                 // &b == &a 依然成立,&b != &d

提示: 口诀:"改变引用对象,同时改变被引用对象。"改变的是值,不是指向

1.2.3 语法层面不开新空间
引用指针
语法层面开空间× 不开√ 开(4/8字节)
sizeof 结果被引用类型大小指针本身大小
本节小结

三大特性(必须初始化、不能改指向、不开空间)是理解引用一切用法的基石。


1.3 引用的价值:两个核心场景

1.3.1 引用传参

优势: ①避免拷贝,提高效率 ②允许函数内部修改外部实参

// 传值:修改无效
void swap_val(int x, int y) { int t=x; x=y; y=t; }

// 引用:正确修改实参
void swap_ref(int& x, int& y) { int t=x; x=y; y=t; }

注意: 调试陷阱:在被调函数的监视窗口里只能看当前帧,main 中的变量显示为旧值不代表没有修改,函数返回后再看才是真实结果。

1.3.2 引用作返回值

传值返回的问题: 生成临时对象(有拷贝开销),临时对象有常性,不能作左值

引用返回的优势: 直接返回对象别名,无拷贝,可作左值直接赋值

// × 传值返回:返回副本,不能左值赋值
int STTop_val(ST& rs) { return rs.a[rs.top-1]; }
// STTop_val(st) = 4;  // 编译报错!

// √ 引用返回:返回别名,可左值赋值
int& STTop_ref(ST& rs) { return rs.a[rs.top-1]; }
STTop_ref(st) = 4;  // 直接修改栈顶元素!

在这里插入图片描述

提示: “这比单独写个 ModifyTop 函数爽多了!以后结合运算符重载,v[i] = 10 就是这个原理。”

注意: 不能返回局部变量的引用!

int& bad_func { int a = 10; return a; }  // × 函数结束 a 销毁 → 类似野指针

判断原则:出了作用域,被返回的对象是否还存在?堆上/全局 √;局部栈变量 ×。

在这里插入图片描述

本节小结

引用传参 = 无拷贝 + 可修改实参;引用返回 = 无拷贝 + 返回值可作左值。两者核心都是"我就是它"。


1.4 const 引用

1.4.1 权限只能平移或缩小,不能放大

权限放大/缩小只存在于引用和指针之间,普通变量赋值(值拷贝)没有此概念。

场景合法性说明
const int a; const int& ra = a;权限平移(都只读)
int b; const int& rb = b;权限缩小(b 可写,rb 只读)
const int a; int& ra = a;×权限放大(a 只读,ra 可写,矛盾!)

在这里插入图片描述

提示: 比喻:“鲁迅(原变量,可读可写)以’周树人’(const引用,只读)身份出现时,需要谨言慎行。但这不影响在家(用 b 本身)依然随意——b++ 仍然有效。**权限缩小的是别名,不是原变量。”

1.4.2 const 引用的特殊能力——引用临时对象

临时对象(Temporary Object):运算、类型转换、传值返回时,编译器产生的未命名中间对象,具有常性(只读)。只有 const 引用才能绑定临时对象。

int a = 1, b = 2;
double d = 3.14;

const int& rc = 30;          // √ 字面常量 → 临时对象,const 可引用
// int& rc = 30;             // × 普通引用不能绑定临时对象

const int& rd = (a + b);     // √ 表达式结果 → 临时对象
// int& rd = (a + b);        // ×

const int& ri = d;           // √ double→int 类型转换产生临时 int 对象
// int& ri = d;              // ×

在这里插入图片描述

注意: const int& ri = dri 引用的不是 d 本身,而是类型转换后的临时 int 对象。const 引用会延长临时对象的生命周期,随引用一起销毁。

哪些情况会产生临时对象:

  • 算术/位运算表达式(a+ba|b
  • 隐式类型转换(doubleint
  • 函数传值返回
1.4.3 const 引用传参的真正价值

提示: “讲这么半天 const 引用,就是为了一个场景:**函数传参。”

const T& val 作为参数,能接受一切类型的参数:

// × 普通引用:限制太多
void func(int& val);
// func(30);    // 传常量失败
// func(a+b);   // 传表达式失败

// √ const 引用:无所不能,且无拷贝
void func(const int& val);
func(a);        // √ 普通变量(权限缩小)
func(a_const);  // √ const 变量(权限平移)
func(30);       // √ 字面常量(临时对象)
func(a + b);    // √ 表达式(临时对象)
func(d);        // √ 类型转换(临时对象)

提示: “以后你们看 push_back(const T&) 这类接口,就是这个道理——既减少拷贝,又接受一切。如果函数内需要修改(如 swap),才不加 const。”

本节小结

const 引用 = 权限缩小的别名。核心价值:绑定临时对象 + 通用传参。


1.5 引用与指针的深度对比

1.5.1 底层窥探

从汇编指令层看,引用和指针生成的代码完全相同:都是取地址、存地址、解引用访问。引用的底层实现就是指针。

注意: 面试时不要**上来就说"引用底层是指针,两者一样"——要先从语法层面讲区别,底层只是辅助理解。

在这里插入图片描述

1.5.2 语法层面的主要区别(面试高频)
对比维度引用指针
语法层面开空间× 不开√ 开(4/8字节)
初始化要求必须初始化建议但非强制
改变"指向"× 不能√ 可以
访问对象直接访问* 解引用
sizeof 含义引用类型大小指针大小(4/8字节)
安全性相对较高容易野指针/空指针
相互替代× 不能完全替代× 不能完全替代

为什么引用不能替代指针:链表中 node->next = newNode 需要修改指针的指向,引用一旦绑定就无法改变,所以动态改变"连接关系"的数据结构必须用指针

提示: “引用和指针是 C++ 的’打虎亲兄弟’——功能有重叠,但各有独特价值,相辅相成。”

本节小结

语法层面区别清晰,底层一致。二者互补,共同解决 C++ 中的各类问题。


第二章 内联函数(inline)

引入

C 语言宏(#define)通过预处理阶段文本替换避免函数调用开销,但充满陷阱。C++ 引入 inline 关键字,作为宏函数的优雅替代者。


2.1 宏函数的缺陷

宏的本质: 预处理阶段的纯文本替换**,不是函数调用。

实现两数相加宏的常见错误(面试高频考点):

// × 各种错误写法
# define ADD(a, b) a + b           // 无括号
# define ADD(a, b) (a + b)         // 只有外括号
# define ADD(a, b) a + b;          // 加了分号(在 if/else 中爆炸)

// √ 正确写法:内外都要加括号
# define ADD(a, b) ((a) + (b))

为什么内外都要加括号

  1. 外括号:控制整体优先级

    • ADD(1,2)*3 展开为 1+2*3 = 7(期望 9)→ 加外括号后 (1+2)*3 = 9
  2. 内括号(参数括号):防止参数是表达式时优先级错乱

    • ADD(a|b, c&d) → 位运算优先级低,不括住会先和加法混算 → 加内括号 √
  3. 不加分号:宏会被展开到 if、三目运算符等表达式中,多余分号破坏语法

提示: “就算能写对宏,你还要能解释清楚每个括号的存在意义。面试官考’精通C语言’,第一关就是考宏。”


2.2 inline 的本质与优势

内联函数(inline function):在函数前加 inline建议编译器在调用处直接展开函数体(不建立栈帧),消除函数调用开销。

// F.h 中定义(注意:直接在头文件定义,不分离到 .cpp)
inline void f(int i) {
    cout << i << endl;
}

inline int Add(int x, int y) {
    return x + y;
}

inline vs 宏

对比维度宏(#defineinline 函数
处理时机预处理(文本替换)编译期(编译器决定展开)
类型检查× 无√ 有
可调试性× 替换后无法步入√ Debug 下可 F11 进入
括号陷阱× 容易中招√ 函数语义,无此问题

2.3 inline 的注意事项

① inline 只是建议,编译器有最终决定权

提示: “编译器是个’有独立思想的大孩子’——你的建议合理我就听,不合理我就忽略。”

  • Debug 模式 :默认不展开(方便调试)
  • Release 模式: 会展开(可通过 VS 属性 → C/C++ → 内联函数扩展 配置)**。
  • 适合 inline :函数体短小(约 5~10 行以内)、频繁调用(如 swap
  • 不适合 inline: 递归函数、代码行数多的函数**。

为什么编译器会拒绝:一个 100 条指令的函数被 1 万处调用:

  • 不展开:~1万 + 100 条指令(共享函数体)
  • 展开:1万 × 100 = 100万条指令(**可执行文件急剧膨胀!)
    在这里插入图片描述
② inline 不建议声明和定义分离到不同文件
// × 错误:声明在 F.h,定义在 F.cpp → 链接错误
// F.h:   inline void f(int i);
// F.cpp: void f(int i) { cout << i << endl; }

原因:编译时只有声明(inline 属性),编译器认为不需要地址,不生成完整指令;链接时找不到地址 → 链接错误,且 inline 属性失效。

√ 正确做法:inline 函数的定义直接写在 .h 头文件中,#include 后即可在调用处展开。

③ 类内直接定义的成员函数默认是 inline
class Stack {
    void push(int x) { _a[_top++] = x; }  // 默认 inline
};
本节小结

inline 是宏函数的优雅替代:有类型检查、可调试、无括号陷阱。适合短小+频繁调用函数。只是建议,编译器有权拒绝。定义必须写在头文件中。


第三章 nullptr——空指针的正确姿势

3.1 NULL 的历史包袱与缺陷

NULL 是宏,在 C++ 中被定义为整数 0(而非 (void*)0,因为 C++ 对 void* 转换更严格):

// C++ 中 NULL 的定义
# define NULL 0   // 就是整数字面量

这导致函数重载歧义

void f(int x);      // 版本1
void f(int* ptr);   // 版本2

f(NULL);    // 期望调用 f(int*),实际调用 f(int)!
            // 因为 NULL 就是整数 0,优先匹配 int

注意: 这是 C++ 的历史遗留问题:NULL 的类型是 int,在重载中会匹配到错误的版本。


3.2 nullptr 的设计哲学

C++11 引入 nullptr 关键字(不是宏,是编译器直接支持的字面常量),彻底解决问题:

  • √ 可隐式转换为任意类型的指针
  • × 不能隐式转换为整型
f(nullptr);        // √ 明确调用 f(int*),类型安全
// int x = nullptr; // × 编译报错:不能转整型

int* p = nullptr;  // √ 替代 NULL/0 初始化指针
对比0 / NULLnullptr
本质整数字面量(宏)C++11 关键字
可转指针
可转整型×(安全!)
重载中匹配整数版本(**注意:有歧义)指针版本(√ 准确)

提示: “从此以后,我们不再用 NULL。初始化指针一律用 nullptr。”

本节小结

nullptr 是 C++11 对 NULL 的替代,类型安全,在函数重载中正确匹配指针参数。


第四章 类和对象·上篇

引入:C++ 对 C 结构体的升级

C 语言结构体只能存数据(属性),行为(方法)分离在外;而且 Stack 的函数和 Queue 的函数容易名字冲突。C++ 把数据和方法装进同一个"箱子",并通过访问控制实现封装——这就是类(class)**。

提示: “以前 C 语言:数据是数据,函数是函数,各干各的。现在 C++ 把它们捏在一起,这个’捏在一起的东西’就叫类。”


4.1 类的定义格式

class 类名 {
    // 成员变量(属性)+ 成员函数(方法)
};   // ← 末尾分号不能省略!

成员变量(Member Variable):类中的数据,描述对象的属性。
成员函数(Member Function):类中的函数,描述对象的行为。

类内定义 vs 类外定义(声明与定义分离)

class Date {
public:
    // 类内定义:默认 inline
    void Print {
        cout << _year << "/" << _month << "/" << _day;
    }
    // 类内声明,类外定义
    void Init(int year, int month, int day);
private:
    int _year, _month, _day;
};

// 类外定义:必须用 类名:: 指定类域
void Date::Init(int year, int month, int day) {
    _year = year; _month = month; _day = day;
}

注意: 类外定义成员函数时,必须加 类名::**,否则编译器把它当作全局函数,找不到成员变量(_year 等不在全局域里)。


4.2 访问限定符

C++ 通过访问限定符控制类成员对外的可见性,这是实现封装的核心手段

访问限定符类内访问类外访问说明
public(公有)对外开放的接口
private(私有)×仅类内使用
protected(保护)×类内可用(继承时与 private 有区别)

注意: 现阶段把 protectedprivate 视为等同,区别留到**继承章节再讲。

作用范围规则:从该限定符开始,到下一个限定符(或类的结束 })为止。

class Stack {
public:           // 以下成员为 public
    void Init;
    void Push(int x);
    int  Top;
private:          // 以下成员为 private
    int* _a;
    int  _top;
    int  _capacity;
};

封装设计惯例

  • 成员变量 → private/protected(不希望外部随意修改数据)
  • 对外使用的成员函数 → public(提供安全接口)
  • 内部辅助函数 → private(不对外暴露)

4.3 类域

C++ 有四种域:局部域、全局域、命名空间域、类域(Class Scope)

类定义后形成独立的类域:

  • **名字隔离:**不同类中可有同名函数(Stack::PushQueue::Push),不会冲突
  • 只影响名字查找,不影响生命周期
  • 类外访问类域中的名字需要 类名:: 限定

注意: 与命名空间的区别:命名空间可用 using namespace xxx; 展开,类域不能展开,必须通过对象或 类名:: 访问。


4.4 成员变量命名规范

成员函数参数与成员变量同名时,代码可读性极差:

// × 可读性差:year 到底是参数还是成员变量?
void Init(int year, int month, int day) {
    year = year;  // 自赋值!根本没有效果!
}

C++ 没有强制规范,惯例上给成员变量加特殊标记:

风格示例说明
前加下划线(推荐)_year本课程采用
后加下划线year_部分公司使用
前加 m_m_yearm = member
// √ 清晰易读
class Date {
    void Init(int year, int month, int day) {
        _year = year; _month = month; _day = day;
    }
    int _year, _month, _day;  // 一眼识别为成员变量
};

4.5 class 与 struct 的异同

C++ 中 struct 也升级成了类(可定义成员函数),与 class唯一区别是默认访问权限

classstruct
默认访问权限privatepublic
其余功能完全相同完全相同

C++ 对 C 结构体的兼容

// C 语言风格(C++ 也支持)
typedef struct ListNodeC {
    int val;
    struct ListNodeC* next;   // 必须写 struct 前缀
} ListNodeC;

// C++ 风格(更简洁,无需 typedef,无需 struct 前缀)
struct ListNodeCPP {
    int val;
    ListNodeCPP* next;        // 类名直接代表类型
};

提示: “以后写链表节点,用 C++ 风格,省去 typedefstruct 前缀,直接 ListNode* next 就搞定。”

什么时候用 struct 所有成员都是公开的、不需要访问控制时(如简单数据聚合)。


4.6 类与对象的关系——图纸与房子

类(Class):是蓝图/设计图,描述"这类事物有什么属性和方法",本身是声明,不存储运行时数据。

对象(Object):用类实例化出来的具体个体,有独立内存,可存储实际数据。

提示: “类就像图纸——图纸不能住人;对象就像按图纸盖的房子——可以住人(存数据)。一张图纸可以盖出 N 栋房子,一个类可以实例化出 N 个对象。”

// 类定义(声明,不开空间)
class Date {
public:
    void Init(int y, int m, int d) { _year=y; _month=m; _day=d; }
private:
    int _year, _month, _day;
};

// 实例化(定义对象,开辟内存)
Date d1;                     // 实例化出对象 d1
d1.Init(2024, 7, 9);        // 通过对象调用成员函数

Date d2;                     // 另一个独立对象
d2.Init(2025, 1, 1);
本节小结

类是封装单元(数据+方法),对象是类的具体实例。类定义是声明(不占内存),实例化才开辟内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值