第一章 基础知识

概述

本专栏为C++的学习笔记,从最基础的学起,每小节均有代码实现,有想学习的同学可以跟着敲一遍。
第一章学习点:

  • 参数传递的不同方式
  • 函数或者方法返回的不同模式
  • 模板函数
  • 递归函数
  • 常量函数内存分配和释放 new 和 delete
  • 异常处理
  • 类与模板类
  • 类的公有成员、保护成员、私有成员
  • 友元
  • 操作符重载
  • 标准模板库

1.1 引言

一个程序判断它是否写的完美我们需要从几个方面看:

  • 它是否正确
  • 它是否易读
  • 它是否有完善的文档
  • 它容易修改吗
  • 它在运行需要多大内存
  • 它的运行时间多长
  • 通用性如何
  • 可以在多种计算机上运行吗

1.2 函数与参数

1.2.1 传值参数

一、示例代码

函数abc

/*
程序1-1
*/
int abc (int a , int b , int c) {
	return  a + b * c;
}
在程序1-1中,a、b、c是函数abc的形参(formal parameter)。

调用:

/*
程序1-2
*/
z = abc(2,x,y)	
其中2,x,y 是对应的实参(actual parameter)。

其原理为调用abc(2,x,y)时,a被赋值2,b被赋值x,c被赋值y。作为值的传递。abc函数中做任何操作均不会影响x,y原本的值。

1.2.2 模板函数

假设我们希望另一个函数来实现类似1-1的表达式,不过这次a、b、c是float类型,结果也是float,此时我们可以使用模板函数。

/*
程序1-3
*/
template<class T>
T abc (T a , T b , T c) {
	return  a + b * c;
}

在C++中,调用模板函数时编译器通常会根据实参类型自动推导模板参数的类型,也可以显式指定模板参数。对于你提供的abc模板函数,有以下几种调用方式:

  1. 自动类型推导调用(最常用):
int result = abc(1, 2, 3);           // 推导T为int
double result2 = abc(1.5, 2.5, 3.5); // 推导T为double
  1. 显式指定模板参数(当需要强制类型或推导不明确时):
auto result3 = abc<double>(1, 2, 3); // 显式指定T为double
  1. 多类型参数的注意事项
// 错误:所有实参必须为同一类型(T)
// auto error = abc(1, 2.5, 3); // 无法同时推导T为int和double

// 正确:显式转换或统一类型
auto correct = abc<double>(1, 2.5, 3); // 显式指定T为double

一、示例代码

以下是完整的调用示例:

#include <iostream>

template<class T>
T abc(T a, T b, T c) {
    return a + b * c;
}

int main() {
    // 自动推导类型
    int intResult = abc(1, 2, 3);           // T=int
    double doubleResult = abc(1.5, 2.5, 3.5); // T=double
    
    // 显式指定类型
    auto mixedResult = abc<double>(1, 2.5, 3); // T=double
    
    std::cout << "intResult: " << intResult << std::endl;
    std::cout << "doubleResult: " << doubleResult << std::endl;
    std::cout << "mixedResult: " << mixedResult << std::endl;
    
    return 0;
}

二、关键要点

  1. 类型一致性:所有传入的实参必须能隐式转换为同一类型T
  2. 自动推导:优先使用自动推导,简洁且不易出错
  3. 显式指定:在类型不匹配或需要强制转换时使用
  4. 返回值类型:返回值类型由模板参数T决定,需注意精度丢失问题

如果需要处理不同类型的参数,可以修改模板函数设计(例如使用多个模板参数)。

1.2.3 引用参数

在 C++ 中,template<class T> T abc(T& a, T& b, T& c) 里的const 引用参数(T&),是可以修改实参的,下面详细解释原理和效果:

一、引用参数的本质

  • 引用(&)可以理解成“变量的别名”,绑定实参后,函数里操作 a/b/c直接等同于操作传入的实参
  • 对比“值传递”(传参时拷贝一份数据给函数),引用传递不拷贝数据,而是直接关联到实参,修改引用就会影响实参。

二、修改实参的示例

看这段代码就清楚了:

/*
程序 1-4
*/
#include <iostream>
using namespace std;

template<class T>
T abc(T& a, T& b, T& c) {
    // 这里对引用参数 a 进行修改
    a = a + 1;  
    return a + b * c;
}

int main() {
    int x = 1, y = 2, z = 3;
    // 调用函数时,a 绑定 x,b 绑定 y,c 绑定 z
    int result = abc(x, y, z);  

    // x 的值被修改了!输出 x=2
    cout << "x = " << x << endl;  
    cout << "result = " << result << endl; 
    return 0;
}

运行后:

  • x 原本是 1,但函数里 a = a + 1 直接修改了 x,所以 x 最终变成 2
  • 计算逻辑 a + b * c 也会用修改后的 a(即 2)参与,结果 result = 2 + 2*3 = 8

三、和 const 引用 的区别

如果参数是 const T&(常量引用),比如:

/*
程序 1-5 利用常量引用参数计算一个表达式
*/
template<class T>
T abc(const T& a, const T& b, const T& c) {
    // 错误!const 引用禁止修改
    // a = a + 1;  
    return a + b * c;
}

加了 const 后,函数里不能修改 a/b/c 关联的实参,能避免意外修改数据,更安全。

四、什么时候用非 const 引用?

  • 需要修改实参时:比如函数要“同时返回计算结果,又修改入参状态”(像某些累加逻辑、交换值场景);
  • 传递大对象,想避免拷贝且要修改它:比如自定义的 Matrix 类,传引用比传值高效,且函数需要修改矩阵内容。

但要注意:如果调用时传的是字面量、const 变量,会编译报错,因为非 const 引用不能绑定到“只读数据”。

简单说:T& 引用参数就像“实参的直连通道”,函数里对引用的修改,会直接反映到实参上,用的时候要小心别不小心改坏了外部数据

1.2.4 常量引用参数

C++ 还提供了另外一种参数传递模式——常量引用(const reference)。这种模式指明的引用参数不能被函数修改。例如,在程序 1-4 中,a、b 和 c 的值没有变化,因此我们可以重写这段代码,如程序 1-5 所示。

/*
程序 1-5 利用常量引用参数计算一个表达式
*/
template<class T>
T abc(const T& a, const T& b, const T& c) 
{
    return a + b * c;
}
用关键字 const 来指明函数不可修改的引用参数,这在软件工程方面具有重要意义。函数头告诉用户该函数不会修改实参。

采用程序 1-6 的语法,我们可以得到程序 1-5 的一个更通用的版本。在新的版本中,每个形参可以是不同的数据类型,而函数返回值的类型与第一个形参类型相同。

/*
程序 1-6 比 程序 1-5 的一个更通用的版本
*/
template<class Ta, class Tb, class Tc>
Ta abc(const Ta& a, const Tb& b, const Tc& c) 
{
    return a + b * c;
}

1.2.5 值返回

函数返回数据时的底层逻辑和使用场景 :

1. 基础:值返回(默认情况)

函数返回一个具体的值时(比如 int func() 返回 return 10; ),返回的本质是复制

  • 函数里计算出的结果存在「局部临时变量」里,函数结束时,这些临时变量、局部变量会被销毁(内存释放)。
  • 为了让调用者拿到结果,会把这个值复制一份,传递到调用环境中。
  • 缺点:如果返回的是复杂对象(比如大的 class ),复制过程会有额外性能开销。

2. 引用返回(& 后缀)

给返回类型加 & ,就变成引用返回(比如 int& func() ),它的核心是「不复制,直接返回引用」:

  • 返回的是实参的引用(可以理解为“别名”),不会额外复制数据。
  • 函数结束时,虽然局部变量会销毁,但引用的实参不在函数局部作用域(比如是调用时传入的外部变量),所以引用依然有效。
  • 场景:想直接操作外部变量、避免值复制的性能消耗时用,但要注意返回的引用不能指向函数内的局部变量(会销毁,引用就失效了)。

3. const 引用返回(const T&

在引用返回前加 const ,变成只读的引用返回

  • 和普通引用返回类似,但返回的引用被 const 修饰,调用者拿到的是「只读别名」,不能直接修改它的值。
  • 场景:常用于返回复杂对象的“只读视图”,既避免复制开销,又防止调用者意外修改原始数据。

简单总结:

  • 想简单传值、不关心性能,用值返回
  • 想直接操作外部变量、追求性能,用引用返回(但别返回局部变量的引用);
  • 想“只读访问”外部变量、又不想复制,用const 引用返回
    这些返回形式的设计,本质是为了灵活控制函数返回数据的“传递方式”和“访问权限”,优化性能或保证数据安全

1.3 捕获异常

异常是表示程序出错的信息,捕获方式如下:

int main(){
	try {cout << abc(2,0,4) << endl;}
	catch (char* e){
		cout << "The parameters to abe were 2,0,and 4"<< end1 ; .
		cout << "An exception has been thrown" << endl;
		cout << e << end1;
		return 1;
	}
	return 0;
}

1.4 动态存储空间分配

1.4.1 操作符new

核心功能

new 是 C++ 实现动态内存分配的关键字,程序运行时“按需”向系统申请内存,返回的是指向分配内存的指针,让你能灵活管理内存(不像数组等静态分配,编译时就固定大小)。

分步逻辑(以 int 类型为例)

  1. 声明指针
    int *y; 先定义一个指针 y,但它还没指向有效内存(野指针状态,直接用会出错 )。

  2. 动态分配内存
    y = new int; 这行是关键!new int 会向系统“要”一块能存 int 的内存,然后把这块内存的地址交给指针 y,此时 y 就指向了合法内存空间。

  3. 使用分配的内存
    *y = 10; 通过指针 y 的“解引用”(*y),操作它指向的内存,给这块空间存值 10

简化写法

为了少写代码,C++ 允许“声明 + 分配 + 赋值”一步到位:

  • int *y = new int(10);
    声明指针 y 的同时,用 new int(10) 分配内存,并直接把 10 放到刚申请的空间里

  • 也可以拆成:

    int *y;
    y = new int(10); 
    

    效果和上面一样,先声明指针,再分配内存并初始化值。

注意

动态分配的内存,用完记得用 delete 释放(delete y; ),否则会造成内存泄漏(系统给的内存一直被占用,程序结束才释放,浪费资源),这也是动态内存管理的“坑点”,需要成对用 newdelete

以上是 C++ 动态内存管理的基础用法。

1.4.2 一维数组

一个长度为n的一维浮点数组可以按如下方式创建,动态存储分配:

float *x = new float[n];

操作符new为n个浮点数分配了存储空间。

1.4.3 异常处理

float *x = new float[n]; 

执行该语句可能会出现,对于n个浮点数,计算机没有足够的内存分配。这样的情况下,操作符new不会分配内存,而是会抛出异常bad_alloc。利用try-catch能够捕获该异常。

float *x ;
try{x = new float [n];}
catch(bad_alloc e){
	cerr << "Out of Memory" <<endl;
	exit(1);
}

1.4.4 操作符 delete

动态分配的内存在不需要的时候我们应该要把它释放

delete y;
delete []x;

1.4.5 二维数组

一、核心概念:指针与二维数组的关系

C++采用多种机制来说明二维数组,但是这些机制大都要求编译的时候就知道二维大小。
形参是一个二维数组时,必须指定其大小。例如a[][10]合法,a[][]不合法
为了使我们使用数组时候大小合适,一般采用动态存储分配

举例:
假设已知列数为5,如何动态分配存储空间
char(*c)[5];
try{c=new char[n][5];}
catch(bad_alloc)
{//仅当new失败时才会进入
    cerr <<"Out of Memory" <<endl;
    exit(1);
}
举例:
假设已知列数也未知,如何动态分配存储空间

首先我们需要明白一个概念,如果列数也是未知的,那不可能仅调用一次new就能创建这个二维数组。要构造这样的二维数组,可以把它看做是若干个行构成的,每一行都是能用一个new来创建的一维数组。
在这里插入图片描述
先明确一个重要逻辑:二维数组在指针层面,本质是“指向指针的指针” 。比如,定义 char **x; ,这里 x 是指向指针的指针,x[0]x[1] 等则是指向每一行首元素的指针。打个比方,把二维数组想象成一个“指针数组”,每个元素又是指向某一行数据的指针,这样就能通过多层指针操作,灵活管理二维数组的行和列 。

二、动态创建二维数组的实现(以程序 1-10 为例)

1. 函数设计思路

我们有一个模板函数 make2DArray ,作用是动态创建类型为 T 的二维数组。参数里,T **&x 是引用传递的二维指针(这样函数内对 x 的修改能影响外部),numberOfRows 是行数,numberOfColumns 是列数 。

2. 分步拆解代码逻辑

程序 1-10
template <class T>
bool make2DArray(T **&x, int numberOfRows, int numberOfColumns) {
    try {
        // 第一步:创建行指针数组
        x = new T *[numberOfRows];  
        // 第二步:为每一行分配列空间
        for (int i = 0; i < numberOfRows; ++i) {
            x[i] = new T[numberOfColumns];  
        }
        return true;
    } catch (bad_alloc) { 
        return false; 
    }
}
  • 创建行指针数组x = new T *[numberOfRows]; 这行代码,先给“行指针”分配空间。比如要创建 3 行的二维数组,就会生成一个包含 3 个指针的数组,每个指针后续指向一行数据的首地址 。
  • 为每行分配列空间:通过 for 循环,遍历每一个行指针 x[i] ,执行 x[i] = new T[numberOfColumns]; 为每一行单独分配列的空间。假设列数是 5,就会给每一行开辟能存 5 个 T 类型元素的空间,最终形成类似图 1-2 里 3×5 数组的存储结构 。
  • 异常处理:用 try - catch 捕获 new 可能抛出的 bad_alloc 异常(内存分配失败时触发),分配成功返回 true ,失败返回 false ,让调用者知道创建结果 。

三、使用场景与注意事项

1. 使用场景

这种动态创建二维数组的方式,特别适合 行数或列数不确定 的场景。比如处理用户输入决定数组大小、读取文件数据动态构建二维存储结构等。相比静态二维数组(如 int arr[3][5] ,大小编译时确定),动态创建更灵活,能根据程序运行时的实际需求分配内存 。

2. 注意事项

  • 内存释放:动态分配的内存,用完必须手动释放!否则会内存泄漏。释放时要“逆序”:先遍历每行,释放行的内存(delete[] x[i] ),最后释放行指针数组(delete[] x ) 。
  • 异常安全:虽然代码里做了异常捕获,但实际应用中,要考虑异常发生后程序的状态。比如内存只分配了一部分就抛异常,需妥善处理未完成的分配,避免内存混乱 。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值