一、函数重载
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);