初识C++(二)

一、函数重载

C++⽀持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态行为为,使用更灵活。C语言是不支持同⼀作用域中出现同 名函数的。

函数重载的基本规则

1.函数名相同:所有重载的函数必须具有相同的函数名。

2.参数列表不同:参数列表必须不同,这可以通过改变参数的数目、类型或顺序来实现。仅通过改变函数返回类型来区分函数重载是不允许的。

3.可以改变返回类型:虽然返回类型不是区分重载函数的依据,但不同的重载函数可以有不同的返回类型。

4.可以抛出不同的异常:不同的重载函数可以声明抛出不同的异常,但这同样不是重载的决定性因素。

5.可以有不同的访问修饰符:比如,一个可以是public,另一个可以是private,但这与重载函数的解析无关。

函数重载示例如下:

cpp 复制代码
#include<iostream>  
using namespace std;

// 1、参数类型不同   
int Add(int left, int right)
{
    cout << "int Add(int left, int right)" << endl;
    return left + right;
}

double Add(double left, double right)
{
    cout << "double Add(double left, double right)" << endl;
    return left + right;
}
cpp 复制代码
// 2、参数个数不同   
void f()
{
    cout << "f()" << endl;
}

void f(int a)
{
    cout << "f(int a)" << endl;
}
cpp 复制代码
// 3、参数类型顺序不同   
void f(int a, char b)
{
    cout << "f(int a,char b)" << endl;
}

void f(char b, int a)
{
    cout << "f(char b, int a)" << endl;
}

void f1(int a = 0) 
{
    cout << "f1(int a = 0)" << endl; 
}
int main()
{
    Add(10, 20);
    Add(10.1, 20.2);
    f();
    f(10);
    f(10, 'a');
    f('a', 10);
    f1(); 
    f1(5); 
    return 0;
}

二、引用

在C++中,它是对C语言的重要扩充。引用可以被视为某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。这意味着,当你通过引用访问或修改数据时,实际上是在访问或修改它所引用的那个变量。

定义:

引用的定义方式与指针类似,但使用&符号来标识,而不是指针的*符号。引用的基本语法如下:

其中,&在此处不是求地址运算,而是起标识作用,表示这是一个引用声明。类型指的是目标变量的类型,引用名是你为这个变量定义的别名,而目标变量名则是被引用的原始变量名。

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
 int a = 0;
 // 引⽤:b和c是a的别名 
 int& b = a;
 int& c = a;
 // 也可以给别名b取别名,d相当于还是a的别名 
 int& d = b;
 ++d;
 // 这⾥取地址我们看到是⼀样的 
 cout << &a << endl;
 cout << &b << endl;
 cout << &c << endl;
 cout << &d << endl;
 return 0;
}

特性:

必须初始化:

引用在声明时必须被初始化,因为它必须是某个已存在变量的别名。你不能先声明一个引用,然后再去初始化它。

唯一性:一旦一个引用被初始化为某个变量的别名,它就不能再被改变为另一个变量的别名。
内存共享:

引用和它引用的变量共享同一块内存空间。这意味着,对引用的操作实际上就是对它所引用的变量的操作。
别名:

虽然从概念上讲,引用像是变量的一个别名,不占据额外的内存空间,但在底层实现上,编译器通常会将引用实现为指向位置不可变的指针(即const指针)。因此,引用在实际上也会占用一定的内存空间。
不支持数组:

不能直接定义引用的数组,因为数组是由多个元素组成的集合,而引用需要指向一个具体的变量。但是,可以定义数组的引用,即引用整个数组。

安全性:通过常引用(const引用),可以确保引用的变量不会被修改,从而增加代码的安全性。
多态:

引用是除指针外另一个可以产生多态效果的手段。一个基类的引用可以指向它的派生类实例,从而实现多态。

使用场景:

引用在C++中有广泛的应用场景,包括但不限于:

作为函数的参数,可以避免传递大型对象时的拷贝开销。

作为函数的返回值,可以返回函数内部变量的别名,但需要注意生命周期问题。

在需要别名的地方,如链表节点的指针可以使用引用来简化代码。

cpp 复制代码
#include <iostream>  
using namespace std;
void swap(int& x, int& y) {
    int temp = x;
    x = y;
    y = temp;
}

int main() {
    int a = 1, b = 2;
    cout << " swap: a = " << a << ", b = " << b << endl;
    swap(a, b);
    cout << " swap: a = " << a << ", b = " << b << endl;
    return 0;
}

三、const引用

const 引用是 C++ 中一种特殊的引用类型,它允许你创建一个对某个对象的引用,但通过这个引用你不能修改被引用的对象。这种机制主要用于保护数据不被意外修改,同时提供对数据的高效访问。

cpp 复制代码
const Type& name = originalObject;

四、指针和引用的关系

1. 语法概念和内存占用

指针:指针可以在声明时不初始化(但这是一个危险的做法,因为未初始化的指针可能指向任意内存位置),也可以在之后的任何时候修改其指向。
引用:引用必须在声明时被初始化,且一旦初始化,就不能再改变其指向(即不能再让引用指向另一个变量)。

2.初始化和修改

指针:指针可以在声明时不初始化(但这是一个危险的做法,因为未初始化的指针可能指向任意内存位置),也可以在之后的任何时候修改其指向。
引用:引用必须在声明时被初始化,且一旦初始化,就不能再改变其指向(即不能再让引用指向另一个变量)。

3. 访问方式

指针:访问指针指向的值时,需要使用解引用操作符*。
引用:引用就像直接使用该变量一样,不需要任何特殊的操作符。

4. sizeof 操作符

指针:无论指向何种类型的变量,指针本身的大小都是固定的(通常是4字节或8字节,取决于平台的地址空间大小)。

引用:在C++中,sizeof对引用的操作实际上返回的是被引用类型的大小,但这并不意味着引用占用了额外的空间,而是标准规定的一种处理方式。

5. 安全性和使用场景

指针:由于指针的灵活性,它们可以被用来实现各种复杂的数据结构和算法,但这也带来了更高的出错风险,如空指针解引用、野指针等问题。
引用:引用提供了一种更安全、更简洁的方式来访问和修改数据,特别是在函数参数传递和返回值时。使用引用可以避免不必要的拷贝,提高效率,同时减少出错的机会。

五、inline函数

定义:

使用inline关键字声明的函数称为内联函数。编译器在编译时尝试将内联函数的调用替换为函数体本身,以减少函数调用的开销。
优点: 可以减少函数调用的开销,提高程序运行效率。
缺点: 如果过度使用或在不适当的情况下使用(如大型函数),可能会导致编译后的代码体积增大,反而降低性能。

与宏的比较
宏: 在C语言中,宏通过预处理器在编译前进行文本替换。这种方法虽然灵活,但容易出错,且不易调试。
inline函数:C++引入inline作为宏的一种更安全、更易于维护的替代方案。与宏相比,inline函数具有类型检查、作用域限制等编译时检查,使得代码更加健壮和易于理解。

编译器行为
不同编译器的差异 :不同的编译器对inline的实现和支持程度可能有所不同。有些编译器可能更积极地内联函数,而有些则可能更加保守。
调试版本:在调试(Debug)模式下,许多编译器默认不内联函数,以便更好地进行调试。如果需要在调试模式下内联函数,可能需要通过编译器的特定选项来启用。

vs编译器debug版本下⾯默认是不展开inline的,这样方便调试,debug版本想展开需要设置⼀下 以下两个地方。

cpp 复制代码
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
 int ret = x + y;
 ret += 1;
 ret += 1;
 ret += 1;
 return ret;
}
int main()
{
 // 可以通过汇编观察程序是否展开 
 // 有call Add语句就是没有展开,没有就是展开了 
 int ret = Add(1, 2);
 cout << Add(1, 2) * 5 << endl;
 return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;
// 实现⼀个ADD宏函数的常⻅问题 
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
// 正确的宏实现 
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号? 
// 为什么要加外⾯的括号? 
// 为什么要加⾥⾯的括号? 
int main()
{
 int ret = ADD(1, 2);
 cout << ADD(1, 2) << endl;
 cout << ADD(1, 2)*5 << endl;
 int x = 1, y = 2;
 ADD(x & y, x | y); // -> (x&y+x|y)
 return 0;
}
cpp 复制代码
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
 cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
// 链接错误:⽆法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z) 
 f(10);

 return 0;
}

六、 nullptr

NULL实际是⼀个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

cpp 复制代码
#ifndef NULL
 #ifdef __cplusplus
 #define NULL 0
 #else
 #define NULL ((void *)0)
 #endif
#endif

nullptr 是 C++11 引入的一个关键字,用于表示空指针常量。在 C++11 之前,空指针通常使用字面量 0 或宏 NULL(定义为 (void*)0 或简单的 0,具体取决于编译器和标准库的实现)来表示。然而,0 和 NULL 都存在类型不明确的问题,因为它们可以被隐式转换为整数或其他指针类型,这可能导致类型安全上的隐患。

nullptr 解决了这个问题,它是一个指针字面量,其类型为 std::nullptr_t,但可以被隐式转换为任何原始指针类型。使用 nullptr 可以使代码更加清晰和类型安全,因为它明确表示了一个空指针,而不是整数或其他类型的值。

cpp 复制代码
int* ptr = nullptr; // ptr 是一个指向 int 的空指针  
if (ptr == nullptr) {  
    // ptr 是空指针,执行相应逻辑  
}  
  
// 假设有一个函数,其参数是指向 int 的指针  
void func(int* p) {  
    if (p == nullptr) {  
        // 处理空指针的情况  
    }  
    // ...  
}  
  
// 调用 func,传入 nullptr  
func(nullptr);
相关推荐
地平线开发者11 分钟前
profiler debug 工具用法与高一致性策略
算法·自动驾驶
编程大师哥16 分钟前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog18 分钟前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb20081126 分钟前
FastAPI APIRouter
开发语言·python
Benszen28 分钟前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆29 分钟前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木31 分钟前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌40 分钟前
算法解题思路指南
算法
MC皮蛋侠客43 分钟前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者1 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶