}
int main()
{
TestFunc(); //10 20 30
TestFunc(1); //1 20 30
TestFunc(1,2); // 1 2 30
//为什么把1给a呢?我们从半缺省参数用法里找答案
}
半缺省参数:不是所有的形参都赋了缺省值,但赋半缺省参数有一定规则: 半缺省参数必须从右往左依次来给出,不能间隔着给,就是前面的可以省略,但一旦给值,后面的都必须都给值 。因此
void TestFunc(int a, int b = 10, int c = 20)√
void TestFunc(int a=10, int b , int c = 20) ×
void TestFunc(int a=10, int b=20 , int c ) ×
通过半缺省参数的规则,我们可回答为什么全缺省参数给值是从前往后给的:半缺省参数前面的可以省略,所以在不知道函数是不是半缺省参数的情况下,实参要赋从第一个形参开始赋值
5. 函数重载
定义:在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
如以下代码:
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10, 20);
Add(10.0, 20.0);
Add(10L, 20L); //通过实参类型来找函数
return 0;
}
注:函数不可仅靠返回值类型来实现重载
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
//这两个函数无法实现重载
注:
缺省函数与无参函数无法形成重载 ,例如:
void TestFunc(int a = 10);
void TestFunc( );
//这两个函数就无法形成重载,在另一个函数中调用TestFunc( ),编译器不知道要调用哪一个;
缺省函数与普通函数无法形成重载,例如:
void TestFunc(int a = 10);
void TestFunc(int a );
//这两个函数就无法形成重载,在另一个函数中调用TestFunc(num ),编译器不知道要调用哪一个;
因而:想要形成函数重载,要确保两个函数在调用的时候不会起冲突,不会出现在传某个值的时候,两个函数都可以调的情况。
我们知道:c语言中不可以实现函数重载,为什么c++中可以呢?因为在程序编译时,编译器会对每个函数名进行命名修饰,下面我们来引入命名修饰的概念
名字修饰
在c++程序编译时,编译器为区分各个函数,会将函数、变量名重新改变,使每个函数名成为全局唯一的名称,将参数类型包含在最终的名字中,因而通过形参列表的不同可以将同名函数进行区分,就可保证名字在底层的全局唯一性。
那么c++中具体将名字修改成什么样子了呢?
有如下代码:
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
//在vs下,对上述代码进行编译链接,最后编译器报错:
//error LNK2019: 无法解析的外部符号 "double cdecl Add(double,double)" (?Add@@YANNN@Z)
// error LNK2019: 无法解析的外部符号 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。
visual stdio 下c++的修饰规则:
C++入门学习
通过以上签名及修饰后的名字可推得命名方式:
修饰后名字由“?”开头,接着是函数名由“@"符号结尾的函数名:后面跟着由“@"结尾的类名“C”和名称空间“N”,再一个“@”表示函数的名称空间结束:第一个“A”表示函数调用类型为“_ cdecl” ,接着是函数的参数类型及返回值,由“@”结束,最后由“Z”结尾。其中A后面第一个是返回值类型,然后接下来到@之前都是形参的类型,H表示int,M表示float
那为什么c语言中,同名函数为什么不能构成重载呢?
因为c语言中的名字修饰只是在函数名前加了个下划线,形参列表并未参与名字修饰,因而不能够通过形参列表来区分各个同名函数。
在某个函数前加extern “C”,可将c++工程中某些函数按c的风格来编译
6. 引用
概念:给变量取了个别名,和变量共用一块内存空间,可以通过引用来改变变量。
定义:类型& 引用变量名=引用实体
注意:引用类型必须和引用实体的类型必须相同。
如:
int a = 10;
int& ra = a;//定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra); //结果相同
引用特性:
1>引用在定义时必须初始化,不能存在空着的引用
int& ra ;//会发生错误
//起了外号,这个外号又不是任何人的,这个外号存在有什么意义?
2>一个变量可有多个引用(一个人可以起很多个别名)
3>引用一旦引用一个实体,再不能引用其他实体
int a=0;
int b=1;
int& ra=a;
ra=b; //ra不是改变了引用,只是将b的值赋给ra
printf("%d",a); //->1
常引用
const int a = 10;
int& ra = a; // 该语句编译时会出错,a为常量
//const修饰的变量,引用前也要加const,若不加,那么就可以通过引用修改变量的值了。
const int& ra = a;//正确写法
int& b = 10; // 该语句编译时会出错,10为常量
//引用不能做常数的引用,要引用前面加const,常熟也是不能够被修改的
const int& b = 10;
double d = 12.34;
int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;//这个是正确的的,但rd并不是d的别名
//而是先通过a来形成一个临时变量存放a的整数部分,然后ra引用这个临时变量。但是该临时变量不知道名字,也不知道地址,因而也修改不了,该临时变量具有一定的常性,因而要在ra前加const
引用使用场景
1>做参数:函数形参设为引用类型
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
说明:如果想要通过形参改变实参,可将形参设为普通类型 如果不想要通过形参改变实参,可将形参设为const类型。
传值、传址、传引用效率比较:
效率:传值的效率低于传址、传引用效率。传地址和传引用时间相同。因为传引用和传指针的过程在内存中的变化其实是一样的,传引用的过程在编译时,会转成传指针的形式,在编译过程中,引用是按照指针方式来实现的
#include
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(int*)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(int&)-time:" << end2 - begin2 << endl;
}
// 运行多次,检测值和引用在传参方面的效率区别
//结果都很小,而且相差无几
//反汇编后,可看到传引用的过程和传指针的过程一模一样。
int main()
{
for (int i = 0; i < 10; ++i)
{
TestRefAndValue();
}
return 0;
}
2>做返回值:将返回值类型设为引用类型
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
注意:如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。因此,引用作为返回值,返回变量不应受函数控制,即函数结束,变量的生命周期存在。比如:全局变量,static修饰的局部变量,用户未释放的堆,引用类型参数
发生该错误有以下代码:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
//在函数调用完后,栈上的c占用的那一块空间就被释放了(可以覆盖),因此就没什么意义了
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <
//->7,Add(3, 4)将c的那一块空间又覆盖掉了
return 0;
}
值和引用的作为返回值类型的性能比较
通过比较,发现传值和指针在作为传参以及返回值类型上效率相差很大,因而可以让引用作为返回值的地方就用引用,除非是要返回一个函数中定义的变量(该变量的空间会随函数调用完而变得无效)要返回值外,其他情况都可用引用返回。
#include
struct A
{
int a[10000];
};
A a;
A TestFunc1()
{
return a;
}
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
// 测试运行10次,值和引用作为返回值效率方面的区别
int main()
{
for (int i = 0; i < 10; ++i)
TestReturnByRefOrValue();
return 0;
}
引用与指针
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间,但在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int x = 10;
int& rx = x;
rx = 20;
int* px = &x;
*px = 20;
return 0;
}
对于该代码我们来看反汇编代码:
C++入门学习
可发现,在内存中两者在底层的使用方式是一样的,引用也是按照指针方式来实现的
那两者又有什么不同呢?
1> 引用在定义时必须初始化,指针没有要求。因而指针需要判空,而引用不用,因为引用定义时就初始化了
2> 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3> 没有NULL引用,但有NULL指针
4>在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
5>引用自加即引用的实体增加1,在连续的空间中指针自加即指针向后偏移一个类型的大小
6>有多级指针,但是没有多级引用
7> 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8> 引用比指针使用起来相对更安全。
7.内联函数
概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
普通函数会进行压栈形成栈帧等操作
C++入门学习
而内联函数在编译时会直接将调用函数换为函数内部的操作
C++入门学习
查看方式:1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,给出vs2013的设置方式):功能->属性->配置->c/c++->将常规中的调试信息格式改为程序数据库,再将优化中的内联函数扩展改为只适用于_inline
特性
1> inline是一种以空间换时间的做法。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2>inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3>inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。因而内联函数具有文件作用域,只在本文件有用,其他文件不可用。
// F.h
#include
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()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?
f@@YAXH@Z),该符号在函数 _main 中被引用
内联函数与const、宏
在c++中,const修饰的变量有常量的特性也有宏的特性,在编译时会发生替换和检测,即使通过指针修改也无法改变变量值。有如下代码
const int a=1;
int *pa=(int *)a;
*pa=2;
printf("%d,%d",*pa,a);
//结果为2,1 a仍然没有修改
而在c中是可以的,因为c中是不会检测的,通过指针也是修改const变量的
宏是在预处理时替换的,不参与编译,也不可调试。
宏的优点:增强代码的复用性。提高性能。
缺点:
1>不方便调试宏。(因为预处理阶段进行了替换)
2>导致代码可读性差,可维护性差,容易误用。
3>没有类型安全的检查 。
因此在c++中,可通过const来代替宏对常量的定义,用内联函数来代替宏对函数的定义
内联函数的优缺点:
https://mp.csdn.net/mdeditor/101083065#
宏定义和内联函数的区别
1 .宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。
内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
2.宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换;内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
3.宏定义是没有类型检查的,无论对还是错都是直接替换; 内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
4.宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率。
8. auto关键字
概念:在C++中,auto作为一个新的类型指示符来定义变量,auto声明的变量是由编译器在编译时期推导而得,变量被赋值什么类型,由初始化的值而定。
特性:
1>使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。
2>auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; //int
cout << typeid(c).name() << endl; //char
cout << typeid(d).name() << endl; //int
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
使用方法
1>auto与指针和引用结合:用auto声明指针类型时,用auto和auto* 没有任何区别,但用auto声明引用类型时则必须加&.
int x = 1;
auto px = &x;
auto *ppx = &x;
auto& rx = x;
auto rrx = x;
cout << typeid(px).name() << endl;
cout << typeid(ppx).name() << endl;
cout << typeid(rx).name() << endl;
cout << typeid(rrx).name() << endl;
rx = 3;
cout << x << endl; //x发生了变化说明是引用
rrx = 2;
cout << x << endl; //x未发生变化,说明不是引用
2>auto在同一行定义多个变量,当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto f = 1, g = 2;
//auto h = 1, i = 2.3; //编译会报错,h和i类型不同
3>auto不能直接用来声明数组
int h[] = { 1, 2, 3 };
//auto t[] = { 4,5,6 };//编译时会发生错误
9. 基于范围的for循环
为什么要引入这个概念?
对一个有范围的集合由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。
用法:for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int arr[] = { 1, 2, 3, 4, 5 };
for (auto& e : arr) //=>for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
e *= 2;
for (auto e : arr) //要对元素值进行改变,变量前要加&,不改变,直接普通变量
cout << e << " ";
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
10.指针空值---nullptr
概念:nullptr指针空值常量,表示指针空值使用nullptr。
为什么要有nullptr,NULL为什么无法用于表示空指针了?
在指针定义时,要初始化(否则会出现野指针),在c中用NULL来给一个没有指向的指针,但其实NULL是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,所以在传空指针时,会出现一些差强人意的错误,如下:
void f(int)
{
cout<<"f(int)"<
}
void f(int*)
{
cout<<"f(int*)"<
}
int main()
{
f(0);
f(NULL); //变成0了,进了第一个函数,但我们NULL想表示指针本是想进入第二个函数
f((int*)NULL);
return 0;
}
因而用nullptr来代替C中NULL在指针中的用法。
并且nullptr也是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:typedef decltype(nullptr) nullptr_t;
注意:
在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同,都是4。
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
深圳网站建设www.sz886.com