函数重载
C++ 支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样 C++ 函数调用就表现出了多态行为,使用更灵活。C 语言是不支持同一作用域中出现同名函数的。
// 参数类型不同
int clic(int a, int b) { return a * b; }
double clic(double a, double b) { return a + b; }
// 参数个数不同
void f() { }
void f(int a) { }
// 参数类型顺序不同
int clic(double a, int b) { return a * b; }
double clic(int b, double a) { return a + b; }
// 数据类型顺序不同
void f1(int a, char b) { }
void f1(char b, int a) { }
还要注意两点:
- 不能将返回值作为重载条件,因为调用时无法区分。
- 这个构成函数重载,但是在编译时会报错,存在歧义,编译器不知道要调用谁。
引用
引用的概念和定义
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用别名 = 引用对象
C++ 中为了避免引入太多的运算符,会复用 C 语言的一些符号,比如前面的<< 和 >>,这里引用也和取地址使用了同一个符号&,注意使用方法角度区分就可以。
int main() {
int a = 10;
// 引用:b 和 c 是 a 的别名
int& b = a;
int& c = a;
b++; // 也可以给 b 取别名,因为 b 还是 a 的别名
int& d = b;
// 在输出里'&'是取地址符号
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
引用的特性
引用在定义时必须初始化。
一个变量可以有多个引用。
引用一旦引用一个实体,再不能引用其他实体。
这里我们透过地址查看,如果地址相同就是引用,不同就是赋值。
int main() {
int a = 15;
int& b = a;
int& c = a;
cout << &b << endl;
cout << &c << endl;
return 0;
}
int main() {
int a = 15;
int& b = a;
int& c = a;
int d = 30;
c = d;
cout << &c << endl;
cout << &d << endl;
return 0;
}
引用的使用
引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
平常我们在调用函数时用的都是传址调用。
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int main() {
int x = 0, y = 3;
cout << x << " " << y << endl;
swap(&x, &y);
cout << x << " " << y << endl;
return 0;
}
我们使用引用就可以不需要使用指针。
void swap(int& x, int& y) {
int t = x;
x = y;
y = t;
}
int main() {
int x = 0, y = 3;
cout << x << " " << y << endl;
swap(x, y);
cout << x << " " << y << endl;
return 0;
}
这就是它的第一个功能:做函数形参,修改形参影响实参。
当然结构体也可以使用。
struct A {
// ..
};
void func(struct A& aa) {}
int main() {
struct A a;
func(a);
return 0;
}
这是它的第二个功能:做函数形参,减少拷贝,提高效率。
这里我们再写一个顺序表:
typedef struct SeqList {
int* a;
int size;
int capacity;
} SL;
void SLInit(SL& sl, int n = 4) {
sl.a = (int*)malloc(n * sizeof(int));
// ...
sl.size = 0;
sl.capacity = n;
}
void SLPushBack(SL& sl, int x) {
// ...扩容
sl.a[sl.size] = x;
sl.size++;
}
int SLAt(SL& sl, int i) {
assert(i < sl.size);
return sl.a[i];
}
我们让他返回之后加 1 会发生什么事?
int main() {
for (size_t i = 0; i < s.size; i++) {
SLAt(s, i) += 1;
}
return 0;
}
我们发现它报错了,这是为啥?
因为返回的是临时变量,临时变量不能被修改。
当我们在返回类型上使用引用会怎么样。
int& SLAt(SL& sl, int i) {
assert(i < sl.size);
return sl.a[i];
}
我们发现不报错了!
因为返回的是别名,别名可以直接影响实参。
这时候就要说引用的第三个功能:引用作为返回值类型,修改返回对象,也能减少拷贝,提高效率。
const 引用
可以引用一个 const 对象,但是必须用 const 引用。const 引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
const 引用必须是另一个 const 的引用。
int main() {
int x = 15;
// 权限不能放大
const int a = x;
const int& b = a;
return 0;
}
如果不是则会报错。
但 const 可以引用非 const 的对象。
int main() {
int y = 0;
int& c = y;
// 权限可以缩小
const int r = y;
return 0;
}
指针同理。
当一个 const 引用时会将数据保存在临时对象中,例如。
此时 y 会将数据保存到临时对象中。
那为什么会报错呢?
因为 C++ 规定临时对象具有常性,因此触发了权限放大,必须要进行常引用。
const 引用也可以引用常数,也可以引用不同类型的数据。
所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++ 中把这个未命名对象叫做临时对象。
指针和引用的关系
C++ 中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数 (32 位平台下占 4 个字节,64 位下是 8byte)。
指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
inline
用 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。
我们怎么看 inline 是否有展开呢?可以透过反汇编进行查看。
这样并没有被展开,因为 vs 的 debug 版本默认不展开。
inline 对于编译器而言只是一个建议,也就是说,你加了 inline 编译器也可以选择在调用的地方不展开,不同编译器关于 inline 什么情况展开各不相同,因为 C++ 标准没有规定这个。inline 适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上 inline 也会被编译器忽略。
C 语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++ 设计了 inline 目的就是替代 C 的宏函数。
vs 编译器 debug 版本下面默认是不展开 inline 的,这样方便调试,debug 版本想展开需要设置一下以下两个地方。
inline 不建议声明和定义分离到两个文件,分离会导致链接错误。因为 inline 被展开,就没有函数地址,链接时会出现报错。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
void f(int i) {
cout << i << endl;
}
// test.cpp
int main() {
/*int ret = Add(3, 5); cout << ret << endl;*/
f(10);
return 0;
}
nullptr
NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++ 中 NULL 可能被定义为字面常量 0,或者 C 中被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,调用了 f(int x),因此与程序的初衷相悖。f((void*)NULL); 调用会报错。
void f(int x) {
cout << "f(int x)" << endl;
}
void f(int* ptr) {
cout << "f(int* ptr)" << endl;
}
int main() {
f(0); // 本想透过 NULL 调用指针版本 f(int*) 函数,结果 NULL = 0,调用了 f(int) 函数,与程序的初衷相悖
f((int*)NULL);
return 0;
}
如果使用 ((void*)NULL) 会报错。
void f(int x) {
cout << "f(int x)" << endl;
}
void f(int* ptr) {
cout << "f(int* ptr)" << endl;
}
int main() {
f(0);
f((void*)NULL);
return 0;
}
C++11 中引入 nullptr,nullptr 是一个特殊的关键字,nullptr 是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被隐式地转换为指针类型,而不能被转换为整数类型。
void f(int x) {
cout << "f(int x)" << endl;
}
void f(int* ptr) {
cout << "f(int* ptr)" << endl;
}
int main() {
f(0);
f(NULL);
f(nullptr);
return 0;
}


