【C++ 笔记】从 C 到 C++:核心过渡 (中)

2025博客之星年度评选已开启 10w+人浏览 1.2k人参与

前言:     

        前文中,我们系统学习了 namespace 机制(有效地解决了命名冲突问题,包含指定访问、部分展开和全部展开三种使用方式),

        同时了解了 cin/cout 输入输出流(具备自动类型识别和支持自定义类型两大优势)

        以及编写了第一个C++程序,本文将为大家解析C语言与C++在函数方面的主要区别。


 

一、缺省参数

      

        这是一个非常实用且常用的特性,允许你在定义函数时为参数指定一个“默认值”。如果调用函数时没有传递该参数,编译器就会自动使用这个默认值。

        

1.1 什么是缺省参数

        

简单来说,就是备胎, 当你在调用函数时:

  • 如果你给了实参,就用你给的。

  • 如果你没给,就用函数定义里预先写好的默认值。

        

基本语法示例:

#include <iostream>
using namespace std;

// 这里 b 被指定为缺省参数,默认值为 10
void printAdd(int a, int b = 10) 
{
    cout << "a: " << a << ", b: " << b << ", sum: " << a + b << endl;
}

int main() 
{
    // 情况 1:只传一个参数
    // a 变成了 5,b 自动使用默认值 10
    printAdd(5);  // 输出: a: 5, b: 10, sum: 15

    // 情况 2:传两个参数
    // a 变成了 5,b 使用传入的 20(覆盖了默认值)
    printAdd(5, 20); // 输出: a: 5, b: 20, sum: 25

    return 0;
}

        

        

1.2 核心规则(非常重要)

        

1.2.1  规则一:必须“从右往左”依次给出

        

如果一个参数有了默认值,那么它右边的所有参数都必须也有默认值,你不能跳着给。

        

 错误写法: void func(int a = 10, int b, int c);

        

原因:a 有默认值,但右边的 b 和 c 没有,编译器不知道你传的参数是给谁的

        

错误写法: void func(int a, int b = 10, int c);

        

原因:b 有默认值,但右边的 c 没有

        

正确写法: void func(int a, int b = 10, int c = 20);

        

从 b 开始往右,全都有默认值

        

        

1.2.2 规则二:声明和定义不能同时给

        

        通常我们会把函数的声明(在 .h 文件中)和定义(在 .cpp 文件中)分开。 但是缺省参数只能出现一次,通常建议写在声明中。

        

// --- header.h ---
//  建议:在声明中指定默认值
void Func(int a = 10); 

// --- main.cpp ---
//  错误:定义中再次指定(即使值一样也不行)
// void Func(int a = 10) { ... } 


//  正确:定义中不要写默认值
void Func(int a)
 {
    cout << a << endl;
}

        

        

1.3缺省参数的分类

        

1.3.1全缺省参数

        

//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

//调用

int main()
{
	//全缺省的四种调用方式
	Func1();      //输出a=10 b=20 c=30
	Func1(1);     //输出a=1 b=20 c=30
	Func1(1, 2);  //输出a=1 b=2 c=30
	Func1(1, 2, 3); //输出a=1 b=2 c=3

    return 0;
}

        

1.3.2半缺省参数

        

//半缺省
//缺省参数从右往左,且不能间隔,跳跃传缺省参数
void Func2(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	//缺省调用,没有缺省值的必须传参
	Func2(100); //输出a=100 b=10 c=20
	Func2(100, 200); //输出a=100 b=200 c=200
	Func2(100, 200, 300); //输出a=100 b=200 c=300
    
    return 0;
}

        

        

1.4 常见的“坑”

        

        缺省参数单独使用很棒,但和一个无参函数混合在一起使用,容易造成二义性(Ambiguity),导致编译报错。

        

#include <iostream>
using namespace std;

// 函数 1:没有参数
void target()
{
    cout << "无参版本" << endl;
}

// 函数 2:带缺省参数
void target(int a = 10) 
{
    cout << "带缺省参数版本: " << a << endl;
}

int main() 
{

    // target(5); // 没问题,只能调用函数 2
    
    // 报错!二义性错误
    // 编译器困惑:是调用函数 1?还是调用函数 2 并使用了默认值?
    target(); 

    return 0;
}

        

        

二、函数重载

        

        函数重载是 C++ 中一个非常实用且基础的特性。简单来说,它允许你在同一个作用域内,给不同的函数起相同的名字,只要它们的参数列表不同即可。

        

2.1 什么是函数重载

        

让我们看一个 print 函数的重载例子:

#include <iostream>
using namespace std;

// 1. 基础函数
void print(int i) 
{
    cout << "正在打印整数: " << i << endl;
}

// 2. 参数类型不同
void print(double d) 
{
    cout << "正在打印浮点数: " << d << endl;
}

// 3. 参数个数不同
void print(int i, int j)
{
    cout << "正在打印两个整数: " << i << ", " << j << endl;
}

// 4. 参数顺序不同 (注意类型要不同才有意义)
void print(int i, double d)
{
    cout << "先整数后浮点: " << i << ", " << d << endl;
}

void print(double d, int i) 
{
    cout << "先浮点后整数: " << d << ", " << i << endl;
}

int main() 
{
    print(10);          // 调用第 1 个
    print(3.14);        // 调用第 2 个
    print(10, 20);      // 调用第 3 个
    print(10, 3.14);    // 调用第 4 个
    return 0;
}

        

        

2.2 构成重载的三个条件

        

编译器区分同名函数的唯一依据是参数列表(也叫函数签名),只要满足以下任意一个条件,就构成重载:

        

①参数个数不同

参数类型不同

参数顺序不同 (指不同类型的参数顺序互换)

        

 例如:同一个 print 函数可以根据参数列表不同,实现不同的函数重载。

1. 基础函数

           

void print(int i) 

        

2.同一个函数,参数类型不同:

        

void print(double d) 

        

3.同一个函数,参数个数不同:

        

void print(int i, int j)

      

4.同一个函数,参数的类型顺序不同:

         

void print(int i, double d)

        

2.3 常见的“陷阱”

        

2.3.1 情况A:返回值类型不同不构成重载

        

// 错误示例!编译器会报错
int func(int a)
{
	return a;
}
void func(int a) {}

 因为在调用函数时,我们经常忽略返回值(例如直接写 func(10);)。

        

 如果两个函数只有返回值不同,编译器看到 func(10) 时,根本无法判断你想调用哪一个(它不知道你是否需要那个返回值)。   

     

2.3.2 情况 B:类型转换导致的歧义   

        

        有时候你写的重载虽然语法正确,但调用时会让编译器“左右为难”,导致编译错误。

        

void test(long a) { ... }
void test(double a) { ... }

int main() 
{
    test(10); // 报错!
}

原因: 由于10int 类型,所以它既可以转成 long,也可以转成 double,编译器觉得两者优先级一样,不知道选谁。

        

2.3.3 情况 C:默认参数导致的歧义        

        

void func(int a) { 
    cout << "A"; 
}

// 第二个参数有默认值
void func(int a, int b = 10)
{ 
    cout << "B"; 
}


int main() 
{
    func(5); // 报错!
}

        

原因: 当你调用 func(5) 时,既符合第一个函数,也符合第二个函数(因为第二个参数可以省略)。编译器无法区分,直接报错。

        

        

三、内联函数

        

在 C++ 中,内联函数 (Inline Function) 是一个旨在提高程序运行效率的特性。

简单来说,它的作用是向编译器发出一个建议:在调用该函数的地方,不要进行普通的“函数跳转”和“上下文切换”,而是直接将函数的代码“复制粘贴”到调用点。

        

3.1 核心概念与工作原理

        

1.  普通函数调用当程序调用一个普通函数时,CPU 需要保存当前状态(压栈),跳转到函数所在的内存地址执行代码,执行完毕后再跳回来(出栈)。这个过程称为函数调用开销,即该过程为开辟函数栈帧

        

2. 内联函数:编译器直接将函数体内的代码插入到调用该函数的地方。这样就消除了调用和返回的开销,但会增加最终可执行文件的大小(代码膨胀),这个过程省略了开辟函数栈帧提高了程序运行时的效率。

        

3.2 语法与使用

        

使用关键字 inline 来声明内联函数。通常,内联函数的定义(具体实现)必须在头文件中,或者在调用之前可见。

        

#include <iostream>
using namespace std;

// 使用 inline 关键字
// 只是定义:告诉编译器 add 长什么样,并且建议内联
inline int add(int a, int b) 
{
    return a + b;
}

int main() 
{
    int x = 10, y = 20;

    // 内联函数在这里展开
    // 编译器会将下面这行代码实际上替换为: int result = a + b;
    int result = add(x, y); 
    
    cout << result << endl;
    return 0;
}

        

3.3 编译器的“拒绝权”

        

        重要的一点是:inline 只是对编译器的一个建议,而不是命令,编译器可以有权忽略这个建议。

        

以下情况下,编译器通常会拒绝内联,而将其作为普通函数处理:

①函数体过大: 包含复杂的逻辑。

        

包含循环语句 (for, while): 循环执行的时间通常远大于函数调用的开销,内联意义不大。

        

包含递归: 递归函数无法无限展开。

        

④包含 switch 或 goto 语句。

        

3.4 内联函数与宏定义

        

        C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。

        

例如:实现宏函数ADD 完成两数相加

#define ADD(a,b) ( (a)+(b) )

        

 由该宏函数可能会引出如下问题:       

①为什么不能加分号? 

        
进行条件判断和输出时会出现问题,如:if( ADD(2,3) > 0) 

        

②为什么要加外⾯的括号? 

        
 运算时的优先级,如:ADD(2,5) * 3  --->   (2) + (5)  * 3 

        

 ③为什么要加⾥⾯的括号?

        
运算时的优先级,如:ADD(x & y, x | y);       --->   ( x & y + x | y )

                

内联函数实现Add,完成两数相加

nline int Add(int a, int b)
{
	return a + b;
}

        

为什么通过内联函数可以很好的替代宏函数?

因为内联函数真正的函数。有类型安全检查,参数处理正确,且由编译器处理而非预处理器。

            

        

3.5 内联函数的注意事项

        

        inline绝对不能声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

        

详细解释:定义了如下三个文件

        

文件一:test.h(声明func函数)

//test.h (声明func函数):  
inline void func();

        

文件二: test.cpp(定义func函数)

//test.cpp (定义func函数):  

#include "test.h" 

inline void func()
{ 
    //...
}

       

文件三:main.cpp (调用func函数)

        

//#include "test.h" 

int main() 
{ 
    func(); 
    return 0;
}

        

关键分析:当编译器编译 main.cpp 时,发生了什么?

①预处理器把 test.h 的内容复制进来了。

        

②编译器看到了 func() 的声明,知道它是一个 inline 函数。

        

③关键点:编译器此时看不到 test.cpp 里的内容!它找不到 func 的大括号 { ... } 里的代码。

        

结果:编译器无法进行内联展开(因为没代码可抄),它只能退而求其次,生成一条普通的汇编指令 call func,寄希望于链接器(Linker)稍后能找到这个函数的定义。

        

核心问题分析:为什么 “找不到” func的函数定义?(链接期问题)

原因①:当编译器去编译 test.cpp文件时,编译器看到了 inline void func() { ... },对于 inline 函数,编译器通常认为:“这个函数是为了给别人内联展开用的,不需要生成一个可以被外部链接调用的‘全局函数符号’(或者说符号是弱符号/仅本地可见)”。

        

        

原因②:当你编译 test.cpp 时,编译器是“与世隔绝”的,它不知道 main.cpp 的存在,也不知道 main.cpp 可能会在那边哭着喊着要调用 func()。当它在 test.cpp 里看到一个 inline 函数定义,却发现 test.cpp 自己根本没用它时,为了节省空间,直接把它优化掉,不生成汇编代码。

        

最终崩溃(链接错误)

当三个文件都编译完成时,链接器将三个文件进行链接:

        

①它拿着 main.obj,看到里面有一个“欠条”:call func(未解析的外部符号)。

        

②它去 test.obj 里找 func 的定义。

        

③找不到! 因为在编译 test.cpp 时,func 作为内联函数并没有生成标准的全局符号。

        

④报错:LNK2019: unresolved external symbol (未解析的外部符号)。

        

正确的做法:因为编译器需要看到代码才能进行“复制粘贴”,所以最好将内联函数的定义写在头文件 (.h) 里。

#ifndef TEST_H
#define TEST_H

// 声明和定义都在头文件里!
inline void func()
{
    // 具体的实现代码...
}

#endif

        

        

        

既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。

                                                                     

评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值