1 函数重载
1.1 定义
要求:
1)同一作用域内
2)函数名相同
3)形参表不同(与形参个数及每个形参类型有关,与形参名无关)
重载关系的函数调用哪个:
根据实参类型和形参类型进行匹配,调用最匹配的函数
// overload_pre.cpp
// 函数之间的关系--重载关系(1.同一作用域内 2.函数名相同 3.形参表不同)
// 形参表是否相同 与 形参名无关 与 形参的个数 以及 每一个对应形参的类型有关
#include <iostream>
using namespace std;
void foo( char* c, short s ) {
cout << "1. foo" << endl;
}
void foo( int i, double d ) {
cout << "2. foo" << endl;
}
void foo( const char* c, short s ) {
cout << "3. foo" << endl;
}
void foo( double d, int i ) {
cout << "4. foo" << endl;
}
// int foo( double i, int d ) {} // error-是否为重载关系和返回值类型无关
// void foo( double i, int d ) {} // error-形参表是否相同 与 形参名无关
int main( void ) {
char* c; short s;
foo( c, s ); // 1
const char* cc;
foo( cc, s ); // 3
int i; double d;
foo( i, d ); // 2
foo( d, i ); // 4
return 0;
}
1.2 重载和隐藏:
-只有同一作用域内的同名函数才涉及重载的关系
-不同作用域的同名函数涉及的是隐藏关系(定义表隐藏可见表)
// overload.cpp 详谈同一作用域
#include <iostream>
using namespace std;
namespace ns1 {
void foo( char* c, short s ) {
cout << "1. foo" << endl;
}
void foo( int i, double d ) {
cout << "2. foo" << endl;
}
}
namespace ns2 {
void foo( const char* c, short s ) {
cout << "3. foo" << endl;
}
void foo( double d, int i ) {
cout << "4. foo" << endl;
}
}
int main( void ) {
using ns2::foo; //名字空间声明,从这行代码开始ns2中foo引入当前作用域(出现在定义表中)
using namespace ns1;//名字空间指令,从这行代码开始ns1中的foo在当前作用域可见(出现在可见表中)
char* c; short s;
foo( c, s ); // 第3个foo将第1个foo函数隐藏
return 0;
}
1.3 重载匹配优先级
1)普通方式调用重载关系的函数:
完全匹配 > 常量转换 > 升级转换(小转大) > 标准转换(大转小) > 自定义转换 > 省略号匹配
2)函数指针方式调用重载关系的函数:
函数指针本身的类型决定其调用哪个版本的重载函数。
工作中建议完全匹配。
// overload2.cpp 重载匹配优先级
#include <iostream>
using namespace std;
void foo( char* c, short s ) { // _Z3fooPcs 完全匹配
cout << "1. foo(char*, short)" << endl;
}
void foo( const char* c, short s ) { // _Z3fooPKcs 常量转换
cout << "2. foo(const char*, short)" << endl;
}
void foo( char* c, int s ) { // _Z3fooPci 升级转换(小转大,没有数据损失)
cout << "3. foo(char*,int)" << endl;
}
void foo( char* c, char s ) { // _Z3fooPcc 标准转换(大转小,可能数据损失)
cout << "4. foo(char*,char)" << endl;
}
void foo( ... ) { // _Z3fooz 省略号(可变长)匹配
cout << "5. foo(...)" << endl;
}
int main( void ) {
char * c; short s;
foo( c,s ); //_Z3fooPcs(c,s)
// 普通方式调用重载关系的函数,根据实参类型和形参类型匹配,来确定调用哪个foo
void(*pfunc)(const char*,short) = foo; // _Z3fooPKcs 定义函数指针,形参可无名
pfunc(c,s); // 函数指针方式调用重载关系的函数,
// 根据函数指针本身的类型,来确定调用哪个foo
return 0;
}
注意上述代码,定义函数指针,可以不写形参名。
1.4 重载揭秘
重载是通过C++换名机制来实现的:
nm a.out 命令 查看函数符号名
通过extern "C" 可以要求C++编译器按照C方式编译函数,即不做换名,当然也就无法重载:
// extern/cal.cpp
extern "C" { //extern "C" {} ,缩不缩进都可
int add( int a, int b ) {
return a + b;
}
int sub( int a, int b ) {
return a - b;
}
}
// extern/main.c
#include <stdio.h>
int main( void ) {
int c = add( 5, 3 );
int d = sub( 5, 3 );
printf("c=%d,d=%d\n", c, d);
return 0;
}
//g++ -c cal.cpp
//nm cal.o //由于extern "C",函数没换名,与c代码中一致
//gcc -c main.c
//nm main.o
//gcc main.o cal.o
//./a.out
2 动态内存(堆内存)分配
可以继续使用标准C库函数malloc()/free(),
free(野指针)后果很严重(段错误,double free),free(空指针)安全:
// new.cpp 动态(堆)内存分配
#include <iostream>
#include <cstdlib>
using namespace std;
int main( void ) {
int* pm = (int*)malloc( 4 );
cout << "*pm=" << *pm << endl; // 初始值为0
free( pm ); // 当这行代码执行结束后,pm指向的堆内存被释放,进而pm变为野指针
pm = NULL; // pm变为空指针
free( pm ); // 给free传递的为野指针,释放野指针后果很严重,释放空指针是安全
return 0;
}
更建议使用new/delete操作符在堆中分配/释放内存:
int* pi = new int; //初始值一般为0
delete pi;
在分配内存的同时初始化:
int* pi = new int( 100 ); //初始值为100
可以数组方式new:
int* pi = new int [4] {10, 20, 30, 40}; // {}方式是11标准才支持的,编译时-std=c++11
想申请16字节,实际多申请4字节,存储数组元素的个数
但也要数组方式delete:
delete[] pi; 加[],才能将多申请的4字节也释放掉
通过new操作符分配N维数组,返回N-1维数组指针:
int (*pa) [4] = new int [3][4]; // 返回值类型是 int (*)[4]
int (*pb) [4][5] = new int [3][4][5]; // 返回值类型是 int (*)[4][5]
不能通过delete操作符释放已释放过的内存。
delete野指针后果很严重(段错误,double free),delete空指针安全。
故建议释放指针指向的内存后,立即置空:
delete(pn);
pn = NULL;
new操作符申请内存失败,将抛出异常 。
// new.cpp 动态(堆)内存分配
#include <iostream>
#include <cstdlib>
using namespace std;
int main( void ) {
int* pm = (int*)malloc( 4 );
cout << "*pm=" << *pm << endl; // 初始值为0
free( pm ); // 当这行代码执行结束后,pm指向的堆内存被释放,进而pm变为野指针
pm = NULL;
free( pm ); // 给free传递的为野指针,释放野指针后果很严重,释放空指针是安全
int* pn = new int(100);
cout << "*pn=" << *pn << endl; // 可以自己指定初始值为100
delete pn; // 当这行代码执行结束后, pn指向的堆内存被释放,进而pn变为野指针
pn = NULL;
delete pn; // 给delete传递野指针,释放野指针后果很严重,释放空指针是安全
int* parr = new int[4]{10,20,30,40};//以数组方式new一块内存,永远返回第1个元素的地址
for( int i=0; i<4; i++ ) {
cout << parr[i] << ' ';
}
cout << endl;
delete[] parr; // 数组方式new的也要以数组方式delete
// 不管是几维数组,都应该当做一维数组看待
int(*p)[4] = new int[3][4]; // 返回值是一维数组类型的指针
delete[] p;
try {
new int[0xFFFFFFFF];
}
catch(...) { //捕获...
}
return 0;
}
//g++ new.cpp -std=c++11
3 左值和右值
C++所有数据,不是左值,就是右值:
左值:能够取地址的值,通常具名
右值:不能取地址的值,通常匿名
// lrvalue.cpp 左值 和 右值
#include <iostream>
using namespace std;
int foo( ) {
int m=888;
return m;
}
int main( void ) {
// 当前作用域的生命期
// 具名内存-->能够取址-->左值|非常左值(无const修饰)
// |常左值 (有const修饰)
int a = 10;
&a;
a = 15;
const int b = 10;
&b;
// b = 15; // error
// 语句级生命期
// 匿名内存-->不能取址-->右值|直接更改右值毫无意义(98/03标准给出结论)
//
10;
// &10; // error
// 10 = 15; // error
/*|888|*/foo( ); // (1)分配一块内存空间 (2)生成跳转指令
// &foo( ); // error
// foo( ) = 15; // error
return 0;
}
4 引用(如影随形,从一而终)
1)引用即内存的别名
int a = 10; // a是内存的真名
int& b = a; // 不是赋值,而是给a起别名!(给引用b,赋真名)
2)C++层面,引用本身不占内存,并非实体,
对引用(别名)的所有操作都是在对目标内存进行操作。
3)引用必须初始化,且不能更换目标。
int c = 20;
b = c; // 仅仅是对引用的目标内存a进行赋值
4)不存在引用的引用
int a = 10;
int& b = a; //别名b,真名a
int& d = b; //别名d,真名a
5)引用的常属性必须和目标的常属性“一致”
const int e = 10;
const int& f = e; // OK
int& g = e; // ERROR
6)可以限定更加严格
int a = 10;
const int& h = a; // OK
// alias.cpp 引用:就是一块内存的别名
#include <iostream>
using namespace std;
int main( void ) {
int a = 10;
int& b = a; // 这并不是利用a的数据给b赋值,而应该理解为 引用b是a所代表内存的别名
b = 20; // 对 引用b赋值,其实就是在对引用b的目标内存(a)赋值
cout << "a=" << a << ", b=" << b << endl;//读取引用b的值,
//其实读取的为引用b的目标内存(a)的值
cout << "&a:" << &a << ", &b:" << &b << endl;
// 取引用b的地址,其实取的为引用b的目标内存(a)的地址
int c = 30;
b = c;
cout << "a=" << a << ", b=" << b << ", c=" << c << endl;
cout << "&a:" << &a << ", &b:" << &b << ", &c:" << &c << endl;
int& d = b; // 这并不是引用的引用,而应该理解为 d和b都是目标内存(a)的别名
cout << "&a:" << &a << ", &b:" << &b << ", &d:" << &d << endl;
const int e = 10;
// int& f = e; // error,别名不可以比真名限定的更加宽松
const int& g = e; // ok
const int& h = a; // ok,别名可以比真名限定的更加严格
return 0;
}
7)引用可以延长右值的生命周期 ,(不非得是常引用,11标准...)
8)常引用 即 万能引用 (常指针 亦即 万能指针)
// alias2.cpp 左值 / 右值 和 引用
#include <iostream>
using namespace std;
int foo( ) {
int m=888;
return m;
}
int main( void ) {
// 当前作用域的生命期
// 具名内存-->能够取址-->左值|非常左值(无const修饰)
// |常左值 (有const修饰)
int a = 10;
int& ra = a; // ok
const int& cra = a; // ok
const int b = 10;
// int& rb = b; // error
const int& crb = b; // ok
// 语句级生命期 (引用可以延长右值的生命期)
// 匿名内存-->不能取址-->右值|直接更改右值毫无意义(98/03标准给出结论)
// |到了11标准会有所不同??????
const int& ri = 10; // ok
const int& rf = /*|888|*/ foo( ); // ok
return 0;
}
5 内联函数
调用普通函数(非内联函数)的问题:
-每个普通函数调用语句都需要发生跳转操作,这种跳转操作会带来时间开销:
内联就是用函数已被编译好的二进制代码,替换对该函数的调用指令。
内联在保证函数特性的同时,避免了函数调用的时间开销:
// inline.cpp 内联函数:编译器的优化策略
#include <iostream>
using namespace std;
void foo( int x ) { // 非内联(普通)函数
cout << "foo(int): " << x << endl;
}
inline void bar( int x ) { // 内联函数
cout << "bar(int): " << x << endl;
}
int main( void ) {
foo( 10 ); // 将此处替换为 跳转指令
foo( 20 ); // ...
foo( 30 ); // ...
bar( 10 ); // 见此处替换为 bar函数编译后产生的二进制指令集
bar( 20 ); // ...
bar( 30 ); // ...
return 0;
}
内联会使文件的体积变大,进而导致进程的内存变大,因此只有频繁调用的简单函数才适合内联。
稀少被调用的复杂函数和递归函数都不适合内联。
inline关键字仅表示期望该函数被优化为内联,但是否适合内联则完全由编译器决定。