从C语言到C++(入门篇)

一、从C语言到C++(基础入门)

        C++ 和C语言虽然是两门独立的语言,但是它们却有着扯也扯不清的关系。早期并没有“C++”这个名字,而是叫做“带类的C”。“带类的C”是作为C语言的一个扩展和补充出现的,它增加了很多新的语法,目的是提高开发效率。

        这个时期的 C++ 非常粗糙,仅支持简单的面向对象编程,也没有自己的编译器,而是通过一个预处理程序(名字叫 cfront),先将 C++ 代码”翻译“为C语言代码,再通过C语言编译器合成最终的程序。

        随着 C++ 的流行,它的语法也越来越强大,已经能够很完善的支持面向过程编程、面向对象编程(OOP)和泛型编程,几乎成了一门独立的语言,拥有了自己的编译方式。

        很难说 C++ 拥有独立的编译器,例如 Windows 下的微软编译器(MSVC)、Linux 下的 GCC 编译器、Mac 下的 Clang 编译器,它们都同时支持C语言和 C++,统称为 C/C++ 编译器。对于C语言代码,它们按照C语言的方式来编译;对于 C++ 代码,就按照 C++ 的方式编译。

        从表面上看,C、C++ 代码使用同一个编译器来编译,所以上面我们说“后期的 C++ 拥有了自己的编译方式”,而没有说“C++ 拥有了独立的编译器”。

从语法上看,C语言是 C++ 的一部分,C语言代码几乎不用修改就能够以 C++ 的方式编译。

1、C++关键字(C++98)

        C++总计63个关键字,C语言32个关键字 ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再 细讲

asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast

2、头文件

        C++为了兼容C,支持所有的C头文件,但为了符合C++标准,所有的C头文件都有一个C++版本的,即去掉.h,并在名子前面加c。如

C语言 C++
stdio.h iostream
math.h cmath
string.h cstring
stdlib.h cstdlib
...... ......

3. 命名空间

3.1 域作用限定符

#include
​
// 全局变量
int name = 10;
int main()
{
    int name = 20;
    printf("%d\n", name);
    printf("%d\n", ::name); // :: 是一个域作用限定符,他会默认去全局变量中寻找变量。
}
​
//---------------------------------------------------------------------------------------
​
#include
#include
// 不影响变量声明周期,只是限定域,编译查找规则(域变量存放在(全局)静态区)。
// 默认的查找规则:现在局部找,找不到就在全局找。
namespace ShaXiang  // 域
{
    int rand = 10;
    int srand = 20;
}
​
int main()
{
    // 这里打印的是地址(域)。
    printf("%p\n", rand);
    // 指定:在ShaXiang这个域中找!
    printf("%d\n", ShaXiang::rand);
    printf("%d\n", ShaXiang::srand);
}
    在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的 .
#include 
#include 
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
printf("%d\n", rand);
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”

3.2 定义与使用

        假设这样一种情况,当一个班上有两个名叫 maye的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者他们父母的名字等等。

        同样的情况也出现在 C++ 中。比如有两个相同的变量m,编译器就无法判断你使用的是哪个变量m。

        为了解决上输入问题,引入了命名空间这个概念,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。本质上,命名空间就是定义了一个范围。 定义方式:

namespace name          //name为自定义命名空间名
{
    //代码声明
}

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

// 1. 正常的命名空间定义 - 可以定义多个命名空间,互不干涉
namespace ShaXiang 
{
    int rand = 10;
    int Add(int x, int y)
    {
        return x + y;
    }
    struct Node
    {
        int data;
        struct Node* next;
    };
}
​
namespace HuXiang
{
    int rand = 20;
    int Add(int x, int y)
    {
        return x*10 + y*10;
    }
    struct Node
    {
        int data;
        struct Node* prev;
        struct Node* next;
    };
}
​
int main()
{
    // ShaXiang命名空间
    printf("%d\n", ShaXiang::rand);
    printf("%d\n", ShaXiang::Add(1,2));
    struct ShaXiang::Node* node1;
    // HuXiang命名空间
    printf("%d\n", HuXiang::rand);
    printf("%d\n", HuXiang::Add(1, 2));
    struct HuXiang::Node* node2;
}
​
//---------------------------------------------------------------------------------------
​
// 1. 正常的命名空间定义 - 可以定义多个命名空间,互不干涉
namespace ShaXiang 
{
    int rand = 10;
​
    namespace HuXiang   // 嵌套
    {
        int rand = 20;
    }
}
​
​
int main()
{
    printf("%d\n", ShaXiang::rand);     // 外层嵌套
    printf("%d\n", ShaXiang::HuXiang::rand);    // 嵌套在内部的
}
​
//---------------------------------------------------------------------------------------
​
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

命名空间的使用有三种方式:

#include  // 麻烦
​
int main()
{
    std::cout << "Hello Word!" << std::endl;
    std::cout << "Hello Word!" << std::endl;
}
#include
using namespace std;
// 这种是完全展开:方便了自己,缺点就是不能域C++库重名!
​
int main()
{
    cout << "Hello Word!" << endl;
    cout << "Hello Word!" << endl;
}
​
#include //(推荐)
using  std::cout;
// 这种是不完全展开:将经常使用的展开!
//                  自己定义的尽量与其避开重命名!
​
int main()
{
    cout << "Hello Word!" << std::endl;
    cout << "Hello Word!" << std::endl;
}

3.3 std

std是什么?

std:C++官方库内容定义的命名空间。

std是个名称空间标示符,C++标准库中的函数或者对象都是在命名空间std中定义的,所以我们要使用标准函数库中的函数或对象都要使用std来限定。

对象cout是标准函数库所提供的对象,而标准库在名字空间中被指定为std,所以在使用cout的时候要加上std: : 。这样编译器就会明白我们调用的cout是名字空间std中的cout。

为什么将cout放到命名空间中?

是因为像cout这样的对象在实际操作中或许会有好几个,比如说你自己也可能会不小心定义了一个对象叫cout,那么这两个cout对象就会产生冲突。

std是C++标准库的命名空间,如何展开std使用更合理呢?

1.在日常练习中,建议直接using namespace std即可,这样就很方便。

2.using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对 象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模 大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。

4、 输入输出

新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物, 那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。

C语言的的输入输出用的主要是scanf()、printf()函数,而C++是使用类对象cin、cout进行输入输出。

#include
#include 
using namespace std;
int main()
{
    int a = 0;
    double b = 0.0;
    char c = 'c';
​
    // 优点,可以自动识别变量的类型,相比C的printf/scanf,哪一个方便用哪一个。
    // << 流输入运算符
    cout << "Hello Word!" << std::endl;
    cout << "Hello Word!" << std::endl;
    // >> 流提取运算符
    cin >> a >> b >> c;
​
    // 比较麻烦的是:比如果打印一些特定的格式
    // c语言:
    printf("int:%d double:%.2lf char:%c\n", a, b, c);
    // c++:
    cout << "int:" << a << " " << "double:" << setprecision(3) << b << " " << "char" << c << endl;
}
​
//---------------------------------------------------------------------------------------
​
// bool类型的输出形式;
#include
using namespace std;
​
int main()
{
    bool bButton = true;
​
    cout << bButton << endl;                // 输出1
    cout << boolalpha << bButton << endl;   // 输出true
    return 0;
}
int:1 double:12.32 char:a
int:1 double:12.3 chara // c++打印格式很麻烦
​
// #include  setprecision(3)
/* ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等
等。因为C++兼容C语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,我
们再配合文档学习。*/
  • cin 输入流对象

  • cout 输出流对象

  • endl 换行,并清空输出缓冲区(end line 结束一行,并另起一行)

  • \n照样可以在cout中使用

说明:

  1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件 以及按命名空间使用方法使用std。

  2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。

  3. <<是流插入运算符,>>是流提取运算符。

  4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。 C++的输入输出可以自动识别变量类型。

  5. 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识, 这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有 一个章节更深入的学习IO流用法及原理。 注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应 头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间, 规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因 此推荐使用+std的方式。

5、基本数据类型

5.1 bool类型

C++和C语言的基本数据类型几乎一样

char    short   int  long long  float   double  unsigned    signed ...

值得注意的是,C语言中虽然也有bool(布尔类型),但是需要包含头文件,而在C++中则不用,直接使用即可。

布尔类型对象可以被赋予文字值true或false,所对应的关系就是真与假的概念,即1,0。

可以使用boolalpha打印出bool类型的true或false

bool cmpare(int a,int b)
{
    return a > b;
}
cout << boolalpha << compare(2,3) << endl;

5.2 强弱类型

  • C语言:强类型,弱检查—— 一般就叫做弱类型了

void* p = NULL;
int* p1 = p;
​
int* pn = NULL;
void* pp = pn;
//无报错,无警告,完美

在C语言中,void*可以和其他类型指针相互转换!

  • C++:强类型,强检查 —— 真正意义上的强类型

void* p = NULL;
int* p1 = p;        //错误    “初始化”: 无法从“void *”转换为“int *”
​
int* pn = NULL;
void* pp = pn;      //正确    任意类型的指针都可以自动转为万能指针

在C++中,void*不能直接转换为其他类型的指针,但是可以把其他类型的指针转为void*

5.3 NULL和nullptr

NULL属于 C 语言中的宏,后来 C++11 引入了 nullptr 关键字,都用来表示空指针。

那问题来了,为什么 C++11 要引入 nullptr 呢?

那必定是 NULL 在某些方面存在某些不足,所以引入了nullptr,下面我们来看一下!

在 C 语言中,NULL是一个宏,被定义为空指针;在C++中,被定义为0,定义形式如下所示:

在C语言中NULL会被定义成(void*)NULL,但是C++不允许直接将 void * 隐式转换到其他类型,NULL 只好被定义为 0。

#ifdef __cplusplus
    #define NULL 0
#else
    #define NULL ((void *)0)
#endif
我们来看一个C++中使用NULL的例子,代码如下所示:

#include
using namespace std;
​
void func(int x)
{
    cout << __FUNCSIG__<< endl;
}
​
void func(char* px)
{
    cout << __FUNCSIG__ << endl;
}
​
int main()
{
    //都调用的整数版本的func函数
    func(2);            //void __cdecl func(int)
    func(NULL);         //void __cdecl func(int)
​
    return 0;
}

从运行结果来看,无论是数字还是NULL都是调用的,参数为int类型的函数,这是毋庸置疑的,C++中NULL就是0。

但是这个结果更本不符合语义,我们传NULL,肯定是想传一个空指针进去的,而不是作为一个整数0,为此C++11引入了新的空指针关键字。

下面我们来修改一下上面的程序,将 NULL 替换为 nullptr,修改后如下所示:

int main()
{
    func(2);            //void __cdecl func(int)
    func(nullptr);      //void __cdecl func(char *)
​
    return 0;
}

修改之后,运行结果正常!

看到这里你应该明白为什么 C++11 引入 nullptr 了吧!就是因为 NULL 在 C++ 程序中容易引起歧义!

5.4 const

语言中的冒牌货

C语言中的const并不是真正的常量,只是表示const修饰的变量为只读。

const int num = 18;
//num = 19          //error:不能修改const 对象
//int arr[num]      //error:数组大小必须是常量

通过指针间接修改只读变量的值:

int* pt = (int*)#
*pt = 19;
printf("%d %d\n", num,*pt);     //output:19 19

可以看到常量it的值已经通过指针被间接改变

C++中的真货

为了兼容C语言做出了什么改变?

int* pt = (int*)#
*pt = 19;
cout << num << " " << *pt << endl;      //output:18 19
  • 明明已经通过指针修改了a值,为什么输出却没有变呢?

  • 解释: C++编译器当碰见常量声明时,在符号表中放入常量,那么如何解释取地址呢?(编译期间即可确定) 编译过程中若发现对const使用了&操作符,则给对应的常量分配存储空间(为了兼容C)

从C语言到C++(入门篇)_第1张图片

 

const 的奇葩情况

当给C++中的常量赋值一个变量时,它又变得和C语言一样了;(在程序运行期间分配内存)

int num = 20;
​
const int a = num;                  //赋值变量
int* p = (int*)&a;
*p = 21;
cout << a << " " << *p << endl;   //output:21 21

const字符指针

在C++中const修饰的指针,不能直接赋值给没有const修饰的指针,需要强制类型转换,或者把被赋值的指针也声明为const

char* name = "maye";        //错误
const char*name ="maye";    //正确
  • 函数参数为字符指针的时候需要特别注意

void show(char* name)
{
    cout << name << endl;
}
void test()
{
    show("maye");   //"const char *" 类型的实参与 "char *" 类型的形参不兼容
    //void show(const char* name)   //请把函数原型里的参数加上const
}

6、变量的初始化

在C++中变量的初始化,又有了奇葩的操作(极度猥琐)

6.1 背景

在C++语言中,初始化赋值并不是同一个概念:

初始化创建变量时赋予其一个初始值。

赋值:把对象(已经创建)的当前值擦除,而用一个新值来代替。

6.2 列表初始化

作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用(在此之前,只是在初始化数组的时候用到)。列表初始化有两种形式,如下所示:

int a = 0;          //常规
int a = { 0 };      
int a{ 0 };

说明:上述的两种方式都可以将变量a初始化为0。

2.1 局限

当对内置类型使用列表初始化时,若初始值存在丢失的风险,编译将报错,如:

int a = 3.14;   //正确,编译器会警告     “初始化”: 从“double”转换到“int”,可能丢失数据
int a = {3.14}; //错误,编译器会报错     从“double”转换到“int”需要收缩转换

6.3 直接初始化

如果在新创建的变量右侧使用括号将初始值括住(不用等号),也可以达到初始化效果

int a(20);

其他实例:

const char* name("maye");
char sex[3]("男");
​
const char* name{ "maye" };
char sex[3]{"男"};
​
cout << name << " "<

7、动态内存分配

在软件开发过程中,常常需要动态地分配和释放内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和释放内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。

malloc的职责仅仅是分配内存,new除了分配内存外,还干一件事,调用构造函数。

free的职责仅仅是释放内存,delete除了释放内存之外,还干一件事,调用析构函数。

  • 申请对象:

Type* pointer = new Type;
//...
delete pointer;

示例:

// C++中动态内存分配使用 new 和 delete
int* pNum = new int;
*pNum = 20;
cout << *pNum << endl;
​
delete pNum;

以上代码输出-842150451,据此可以知道,new是不会自动初始化内存的,那么我们可以在new的时候,指定初始值,简单方便!

// C++中动态内存分配使用 new 和 delete
int* pNum = new int(20);
cout << *pNum << endl;
​
delete pNum;

  • 申请对象数组:

Type* pointer = new Type[N];
//...
delete[] pointer;   //数组的释放必须加上[]

示例:

// 申请数组
char* name = new char[10];
name = "ShaXiang";          // error C2440: “=”: 无法从“const char [9]”转换为“char *”
delete[] name;
// 申请数组
char* name = new char[10];
strcpy(name, "ShaXiang");       // 这样是正确的初始化方式。
cout << name << endl;
delete[] name;
​
// 申请数组
char* name = new char[10] {"ShaXiang"}; // 申请时进行初始化。
​
cout << name << endl;
delete[] name;

  • 定位放置

一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置时根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在程序员指定的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。

定位放置new操作的语法形式不同于普通的new操作。例如,一般都用如下语句A* p=new A;申请空间,而定位放置new操作则使用如下语句A* p=new (ptr) A;申请空间,其中ptr就是程序员指定的内存首地址。

Type* pointer = new(ptr) Type;
//根据情况是否释放内存

示例:

// 定位位置
int num = 100;
​
int* pNum = new(&num) int;
cout << *pNum << endl;                  // 正确操作。
​
// 注意:放置的内存一定要大于等于申请的内存。
double* pNum1 = new(&num)double;        // 越界,这样是错误的。
cout << *pNum1 << endl; 

通过定位放置new,把对象a所在的空间首地址,返回了回来,所以输出的值也是123。在这里不需要释放内存哦!

小结:

  • new 和 malloc不要混用

  • 分配内存使用完,记得释放内存(数组和普通变量释放有些微区别)


8、三目运算符

三目运算符,又名条件运算符。可以在合适的情况下,代替if...else...语句,让代码变得更简洁。

C语言和C++中的条件表达式的值的类型是不一样的,C语言中返回的是一个值,也就是常量;C++中返回的是变量本身;这就

void test()
{
    int a = 2;
    int b = 3;
    int max = (a>b?a:b);    //获取ab中最大的值                C √  C++ √
    (a>b?a:b) = 66;         //把ab中最大的那个变量,赋值为66    C ×  C++ √
}

通过代码测试发现,无论是在C语言还是C++中,条件表达式都可以作为一个值,赋值给其他变量;

但是,C语言中的条件表达式不能作为左值,即不能赋值,而在C++中却是可以的。

思考:为什么呢?既然说C++中返回的是变量的本身,俺么在C语言中如何模拟呢?

*(a > b ? &a : &b) = 520;

可以在条件表达式中返回变量的地址,返回之后解引用,即可达到和C++中一样的效果,那么说明C++中是自动帮我们做了这件事情的,绝绝子!

9、缺省参数

9.1 缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。

#include
#include 
using namespace std;
// 这种是不完全展开:将经常使用的展开!
//                  自己定义的尽量与其避开重命名!
​
​
void Func(int iVar = 10)
{
    cout << iVar << endl;
}
​
int main()
{
    Func();     // 没有传参时,使用参数的默认值
    Func(100);  // 传参时,使用指定的实参
​
    return 0;
}

9.2 缺省参数分类

  • 全缺省参数

#include
#include 
using namespace std;
// 这种是不完全展开:将经常使用的展开!
//                  自己定义的尽量与其避开重命名!
​
​
void Func(int iVarA = 10, int iVarB = 20, int iVarC = 30)
{
    cout << iVarA << endl;
    cout << iVarB << endl;
    cout << iVarC << endl;
}
​
int main()
{
    Func();     // 全缺省参数
​
    return 0;
}

  • 半缺省参数

#include
#include 
using namespace std;
// 这种是不完全展开:将经常使用的展开!
//                  自己定义的尽量与其避开重命名!
​
​
void Func(int iVarA = 10, int iVarB = 20, int iVarC = 30)
{
    cout << iVarA << endl;
    cout << iVarB << endl;
    cout << iVarC << endl;
}
​
int main()
{
    Func(100,200);      // 半缺省参数
​
    return 0;
}
  • `注意

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给

  2. 缺省参数不能在函数声明和定义中同时出现

  3. 缺省值必须是常量或者全局变量

  4. C语言不支持(编译器不支持)

10、引用

10.1 什么是引用?

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称来操作变量。对引用的操作与对其所绑定的变量或对象的操作完全等价。

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

Type &refName = variable_name;

10.2 创建引用

先来定义一个变量。

int i = 18;

再为变量i声明一个引用。

int& r = i;

在这些声明中,& 读作引用。因此,第一个声明可以读作 "r 是一个初始化为 i 的整型引用";

cout< 
   

i和r的值都为18,因为他们两个其实都是同一块内存空间的名字。

r = 20;
1cout< 
   

当通过引用修改了值之后,i的值也会发生变化,都输出20。

// 类型& 引用变量名(对象名) = 引用实体;
int i = 100;
int& rI = i;
cout << &rI << " " << &i << endl;   // 相同地址。
cout << rI << " " << i << endl;     // 相同内容。

注意:引用类型必须和引用实体是同种类型的

常量引用

void TestConstRef()
{
    const int a = 10;
    //int& ra = a; // 该语句编译时会出错,a为常量
    const int& ra = a;
    
    const int b = 1;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

注意事项

  • 引用必须初始化

int& refa;      //错误 没有初始化
int a = 8;
int& refa = a;  //正确 
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象

int a = 8,b = 9;
int& refa = a;
refa = b;       //只是把b的值赋值给了refa,而不是让refa引用b
  • 如果要引用右值,那么必须使用常量引用

int& refc = 12;     //错误 “初始化”: 无法从“int”转换为“int &”,非常量引用的初始值必须为左值
const int&refc =12; //正确 
  • 当然,也可以使用右值引用来引用常量;或者使用std::move()把左值转成右值

    引用右值

    // 右值引用。
    int&& refa = 20;
    refa = 100;             // 右值引用可以改变
    cout << refa << endl;   

    引用经过std::move()转换过的变量

    // std::move()将右值转换为左值。
    int age = 20;
    int&& refAge = move(age);
    ​
    refAge = 200;
    cout << age << " " << refAge << endl;

    常引用和右值引用有什么区别呢?

    1,常引用引用的值是不可以修改的;但是右值引用引用的值是可以修改的!(大多数情况用常引用:函数参数)

    2,右值引用一般用来实现移动语义(资源权限的转移)

  • 通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护

引用的用处

  • 作为函数参数

//在函数内部改变实参的值需要传变量的地址
void fun(int* n)
{
    *n=18
}
//指针是非常危险的,因为指针所指向的内存空间,不确定,需要额外判断
fun(nullptr);   //传nullptr 会发生中断,当然,你可以在函数里面判断是否是空,但是如果是野指针呢?
​
//在C++中,除了使用指针外,还可以通过引用来达到这个目的
void fun(int& n)
{
    n=18
}
//可以用指针的引用替代二级指针
​
//***************************************************************************************
​
#include
using namespace std;
​
void Swap(int* num1, int* num2)     // 指针
{
    int temp = *num1;
    *num1 = *num2;
    *num2 = temp;
}
​
void RSwap(int& num1, int& num2)    // 引用
{
    int temp = num1;
    num1 = num2;
    num2 = temp;
}
​
int main()
{
    int a = 100;
    int b = 200;
    Swap(&a, &b);       // 通过指针
    cout << a << " " << b << endl;
​
    int c = 100;
    int d = 200;
    RSwap(c, d);        // 通过引用
    cout << c << " " << d << endl;
    return 0;
}
  • 作为函数返回值

引用返回和传值返回的区别:

传值返回中间会多一层拷贝,一般数据小会存储在寄存器中。

引用返回会减少这一层拷贝,

// 错误示范
int& getAge()
{
    int age = 18;
    return age;     //注意:不要返回局部变量的引用或地址,可以使用静态变量或全局变量替代
}
int& refAge = getAge();
refAge = 23;
​
//***************************************************************************************
​
// 正确的使用方法
#include
using namespace std;
​
int Count1()            // 非引用
{
    int n = 1000;
    cout << &n << endl;
​
    return n;
}
​
int& Count2()           // 引用
{
    static int n = 100;
    cout << &n << endl;
​
    return n;
}
​
int main()
{
    int ret1 = Count1();
    cout << ret1 << endl;
​
    int ret2 = Count2();
    cout << ret2 << endl;
    return 0;
}

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。

引用的本质

引用如此神奇,那么引用的本质到底是什么呢?

  • 引用在C++中,内部实现是一个常指针:type &name <==> type*const name

  • C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。

  • 从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏(所以我们查看不了引用的地址)

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

#include
#include 
using namespace std;
​
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(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
​
int main()
{
    TestRefAndValue();
    return 0;
}
​
// 结果打印:
TestFunc1(A)-time:16
TestFunc2(A&)-time:1

值和引用的作为返回值类型的性能比较

#include
#include 
using namespace std;
​
#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;
}
​
int main()
{
    TestReturnByRefOrValue();
    return 0;
}
​
// 结果打印:
TestFunc1 time:349
TestFunc2 time:4

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

// 语法上:ra是a的别名,不开辟空间。
// 底层实现,引用是使用指针实现的。
int main()
{
    int a = 10;
    int& ra = a;
    ra = 20;
​
    int* pa = &a;
    *pa = 20;
    return 0;
}

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用在定义时必须初始化,指针没有要求

  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体

  4. 没有NULL引用,但有NULL指针

  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  9. 引用比指针使用起来相对更安全

9. 枚举类型

C语言和C++语言都提供了枚举类型,两者是有一定区别。

有如下定义:

enum SHAPE {CIRCLE,RECT,LINE,POINT};
enum WEEK  {MON,TUE,WED,THI,FIR,SAT,SUN};

1,C语言中的enum

  • 允许非枚举值赋值给枚举类型,允许其他枚举类型的值赋值给另一个枚举类型

    enum WEEK today = 3; //正确 today = CIRCLE; //正确

  • 枚举具有外层作用域,容易造成名字冲突(在不同作用域不会冲突,但是遵循就近原则,访问不到外层作用域的枚举)

enum OTHER { RECT };//error C2365: “RECT”: 重定义;以前的定义是“枚举数”
int RECT = 12;      //同上
  • 不同类型的枚举值可以直接比较

if (CIRCLE == MON)
{
    printf("oh.yes");
}

2,C++中的enum

  • 只允许赋值枚举值

enum WEEK today = 3;    //错误    error C2440: “初始化”: 无法从“int”转换为“main::WEEK”
today = CIRCLE;         //错误    error C2440: “=”: 无法从“main::SHAPE”转换为“main::WEEK”
  • 枚举元素会暴露在外部作用域,不同两个枚举类型,若含有相同枚举元素,则会冲突

enum OTHER { RECT };    //错误    error C2365: “RECT”: 重定义;以前的定义是“枚举数”
int RECT = 12;          //错误同上  但是可以通过枚举名访问指定的枚举属性
OTHER::RECT;            //正确
​
// 枚举举例
enum Type
{
    Char,
    Int,
    Float,
    Double,
    Bool
};
​
int main()
{
    // 枚举类型的初始化。
    Type type = Type::Char;
    cout << type << endl;
​
    // 枚举类型在前面指定枚举类型进行赋值。
    int type2 = 0;
    type2 = Type(Type::Bool);
    cout << type2 << endl;
​
    // 枚举类型直接复制。
    int type3 = 0;
    type3 = Double;
    cout << type3 << endl;
    return 0;
}
  • 不同类型的枚举也可以直接比较

if (CIRCLE == MON)
{
    cout<<"oh.yes";
}

3,C++中的 enum class 强枚举类型

enum class SHAPE {CIRCLE,RECT,LINE,POINT};
enum class WEEK  {MON,TUE,WED,THI,FIR,SAT,SUN};
  • 强枚举类型不会将枚举元素暴露在外部作用域,必须通过枚举名去访问

cout< 
  
  • 不相关的两个枚举类型不能直接比较,编译报错

if (SHAPE::CIRCLE == WEEK::MON) //error C2676: 二进制“==”:“main::SHAPE”不定义该运算符或到预定义运算符可接收的类型的转换
{
    cout<<"oh.yes";
}

小结

  • C 枚举类型支持不同类型枚举值之间赋值、以及数字赋值、比较,并且具有外层作用域。

  • C++ 中枚举不允许不同类型的值给枚举类型变量赋值,但仍然支持不同类型之间枚举进行比较,枚举符号常量具有外层作用域。

  • C++ 强枚举类型不允许不同类型之间的赋值、比较,枚举常量值并不具有外层作用域。

10. auto自动类型推导

在 C++11 之前的版本中,定义变量或者声明变量之前都必须指明它的类型,比如 int、char 等;但是在一些比较灵活的语言中,比如 JavaScript、PHP、Python 等,程序员在定义变量时可以不指明具体的类型,而是让编译器(或者解释器)自己去推导,这就让代码的编写更加方便。

C++11 为了顺应这种趋势也开始支持自动类型推导了!C++11 使用 auto 关键字来支持自动类型推导。

注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

  • 使用 auto 类型推导的变量必须马上初始化

  • auto 不能在函数的参数中使用(但是能作为函数的返回值)

  • auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中

  • auto 关键字不能定义数组

  • auto 不能作用于模板参数

10.1 类型别名思考

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写

  2. 含义不明确导致容易出错

#include 
#include 
int main()
{
    std::map m{ { "apple", "苹果" }, { "orange",
    "橙子" },
    {"pear","梨"} };
    std::map::iterator it = m.begin();
    while (it != m.end())
    {
    //....
    }
    return 0;
}

std::map::iterator 是一个类型,但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:

#include 
#include 
typedef std::map Map;
int main()
{
    Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
    Map::iterator it = m.begin();
    while (it != m.end())
    {
    //....
    }
    return 0;
}

使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:

typedef char* pstring;
int main()
{
    const pstring p1; // 编译成功还是失败?
    const pstring* p2; // 编译成功还是失败?
    return 0;
}

在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

10.2 auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么? ​ C++11中,标准委员会赋予了auto全新的含义即: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;
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;
    //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
    return 0;
}

【注意】 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

10.3 auto的使用细则

1.auto与指针和引用结合起来使用 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main()
{
    int x = 10;
    auto a = &x;
    auto* b = &x;
    auto& c = x;
    cout << typeid(a).name() << endl;
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    *a = 20;
    *b = 30;
    c = 40;
    return 0;
}

2.在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

10.4 auto不能推导的场景

1.auto不能作为函数的参数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2.auto不能直接用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}

3.为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

4.auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用。

11. for循环

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。 因此C++中引入了基于范围的for循环,for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

int arr[]={1,2,3,4,5,6,7};
//一般用法
for(int i=0;i
using namespace std;
​
int main()
{
    int _array[] = { 1,8,13,4,87,2,1,48,6,4, };
    // auto_for 获取修改数组
    for (auto& i : _array)
    {
        i *= 2;
    }
​
    // auto_for 获取遍历数组
    for (auto i : _array)
    {
        cout << i << " ";
    }
    cout << endl;
​
    return 0;
}

特点:

  • 从数组的第一个元素开始,逐个赋值给迭代变量

  • 不依赖于下标元素,通用

12. 类型信息

typeid 运算符用来获取一个表达式的类型信息。

typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:

typeid( dataType )
typeid( expression )
    
// typeid.name();
// 直接访问
auto num1 = 17;
auto num2 = 3.14F;
auto num3 = 3.14;
cout << typeid(num1).name() << " | " << typeid(num2).name() << " | " << typeid(num3).name() << endl;
​
// 创建类型访问
auto& n1 = typeid(num1);
auto& n2 = typeid(num2);
auto& n3 = typeid(num3);
cout << n1.name() << " | " << n2.name() << " | " << n3.name() << endl;
​
int main()
{
    auto sum = "Hello";
    auto& y = typeid(sum);
​
    cout << y.name() << endl;       // 类型名
    cout << y.hash_code() << endl;  // 类型唯一标识符
​
    return 0;
}
​
// 骚操作
​
#define COMPARE(type1,type2)\
(typeid(type1) == typeid(type2))
​
int main()
{
    cout << boolalpha << COMPARE(int, char) << endl;
    cout << boolalpha << COMPARE(int, double) << endl;
    cout << boolalpha << COMPARE(int, int) << endl;
    cout << boolalpha << COMPARE(int*, int) << endl;
    return 0;
}

dataType 是数据类型,expression 是表达式,这和 sizeof 运算符非常类似,只不过 sizeof 有时候可以省略括号( ),而 typeid 必须带上括号。

typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。

//获取一个普通变量的类型信息
int n = 100;
const type_info& nInfo = typeid(n);
cout << nInfo.name() << " | " << nInfo.raw_name() << " | " << nInfo.hash_code() << endl;
​
//获取一个字面量的类型信息
const type_info& dInfo = typeid(25.65);
cout << dInfo.name() << " | " << dInfo.raw_name() << " | " << dInfo.hash_code() << endl;
​
//获取一个普通类型的类型信息
const type_info& charInfo = typeid(char);
cout << charInfo.name() << " | " << charInfo.raw_name() << " | " << charInfo.hash_code() << endl;
    
//获取一个表达式的类型信息
const type_info& expInfo = typeid(20 * 45 / 4.5);
cout << expInfo.name() << " | " << expInfo.raw_name() << " | " << expInfo.hash_code() << endl;

本例中还用到了 type_info 类的几个成员函数,下面是对它们的介绍:

  • name() 用来返回类型的名称。

  • raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。。

  • hash_code() 用来返回当前类型对应的 hash 值。hash 值是一个可以用来标志当前类型的整数,有点类似学生的学号、公民的身份证号、银行卡号等。不过 hash 值有赖于编译器的实现,在不同的编译器下可能会有不同的整数,但它们都能唯一地标识某个类型。

除此之外,还可以用 == 比较两个类型是否相等

如有以下定义:

char *str;
int a = 2;
int b = 10;
float f;

类型判断结果为:

类型比较 结果 类型比较 结果
typeid(int) == typeid(int) true typeid(int) == typeid(char) false
typeid(char*) == typeid(char) false typeid(str) == typeid(char*) true
typeid(a) == typeid(int) true typeid(b) == typeid(int) true
typeid(a) == typeid(a) true typeid(a) == typeid(b) true
typeid(a) == typeid(f) false typeid(a/b) == typeid(int) true

13. 函数

内联函数

函数调用时,需要跳转到函数的地址去执行,执行完成后返回到被调用函数,比较费时,因此,C++中提供了一种操作方式,允许编译时直接把函数替换到调用处,即内联函数。在函数前面加上inline申明为内联函数。

为什么使用内联函数? 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)

注意:

  • 内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。

  • C++编译器不一定准许函数的内联请求!(只是对编译器的请求,因此编译器可以拒绝)

  • 现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译 C++中内联函数的限制:

    • 不能存在任何形式的循环语句

    • 不能存在过多的条件判断语句

    • 函数体不能过于庞大

    • 不能对函数进行取址操作

    • 编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。

// 内联函数
inline int Max(int num1, int num2)
{
    return num1 > num2 ? num1 : num2;
}
​
int main()
{
    int num1 = 20;
    int num2 = 30;
    cout << Max(num1, num2);
    return 0;
}

函数默认参数

定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。

void showX(int x = 666)
{
    cout<<"x:"<

小结:

  • 有函数声明时,默认参数可以放在声明或定义中,但不能同时存在

int add(int a,int b = 5);
int add(int a,int b)
{
    return a+b;
}
  • 在具有多个参数的函数中指定默认值时,默认参数都必须出现在不默认参数的右边,一旦某个参数开始指定默认值,它右边的所有参数都必须指定默认值.

    也就是说,函数声明时,必须按照从右向左的顺序,依次给与默认值。

int foo(int a, int b = 2, int c = 3);     // 正确
int foo1(int a, int b = 2, int c);         // 错误, i3未指定默认值
int foo2(int a = 1, int b, int c = 3);     // 错误, i2未指定默认值

占位参数

定义函数时,还可以给函数提供占位参数

  • 占位参数只有参数类型,而没有参数名

  • 在函数体内部无法使用占位参数

  • 占位参数也可以指定默认参数

void func(int a,int = 0)
{
    cout<

14、函数重载

14.1 函数重载的概念

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,对于程序的可读性有很大的好处。

不同参数列表:

  • 参数个数不同

  • 参数类型不同

  • 参数顺序不同

  • 函数重载与返回值类型无关

来个例子体会一下,比较不同类型的两个变量的大小

int maxmum(int a, int b)
{
    return a > b?a:b;
}
long maxmum(long int a, long int b)
{
    return a > b ? a : b;
}
char maxmum(char a, char b)
{
    return a > b ? a : b;
}
double maxmum(double a, double b)
{
    return a > b ? a : b;
}
const char* maxmum(const char* str1,const char* str2)
{
    return strcmp(str1, str2)==1?str1:str2;
}
char* maxmum(char* str1, char* str2)
{
    return strcmp(str1, str2) == 1 ? str1 : str2;
}
​
int main()
{
    cout << maxmum(2, 6) << endl;
    cout << maxmum(2L, 6L) << endl;
    cout << maxmum('A', 'C') << endl;
​
    cout << maxmum("maye", "MAYE") << endl;
    char str1[] = "hello";
    char str2[] = "hello";
    cout << maxmum(str1, str2) << endl;
    return 0;
}

函数重载可以根据具体的参数去决定调用哪一个函数。

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。

// 参数类型不同
int Add(int numA, int numB)
{
    return numA + numB;
}
​
double Add(double numA, double numB)
{
    return numA + numB;
}
​
int main()
{
    int a = 10;
    int b = 20;
    cout << Add(a, b) << endl;
​
    double c = 20.23;
    double d = 24.141;
    cout << Add(c, d) << endl;
    // 在此,我们可以看到cout其实底层就是调用了函数重载!
    return 0;
}
​
​
void Swap(int* numA, int* numB)
{
    int temp = *numA;
    *numA = *numB;
    *numB = temp;
}
​
void Swap(double* numA, double* numB)
{
    double temp = *numA;
    *numA = *numB;
    *numB = temp;
}
​
int main()
{
    // 此处就不演示了
    return 0;
}
​
//---------------------------------------------------------------------------------------
​
// 2、参数个数不同
void f()
{
    cout << "f()" << endl;
}
void f(int a)
{
    cout << "f(int a)" << endl;
}
​
int main()
{
    f();    // 个数不同
    f(1);
    return 0;
}
​
//---------------------------------------------------------------------------------------
​
// 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;
}
​
int main()
{
    f(10, 'a');
    f('a', 10);
}

14.2 为什么需要函数重载?

  • 试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个maxmum函数取不同的名字,如maxmum_int、maxmum_string等等。这里还只是简单的几种情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,这样做很不友好!

  • 类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!

  • 操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!

14.3 重载函数的调用匹配规则

为了估计哪个重载函数最适合,需要依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针;

  • 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double

  • 使用标准转换匹配:如int 到double、double到int、double到long double、Derived到Base、T到void、int到unsigned int;

  • 编译器傻了:如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)

14.4 函数重载遇上默认参数

在给重载函数指定默认参数时,要考虑是否会和别的重载函数冲突

void fun(int a)
{
    cout << "fun(int a) " << a << endl;
}
void fun(int a, int b = 8)
{
    cout << "fun(int,int =8) " << a <<" "<< b << endl;
}
int main()
{
    //fun(5);   //error C2668: “fun”: 对重载函数的调用不明确
      fun(5, 6);//正确
    return 0;
}

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重 载了。 比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个 是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!

14、指针空值nullptr(C++11)

14.1 C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}

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)"<

程序本意是想通过f(NULL)调用指针版本的f(int)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。 ​ 在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。 注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

你可能感兴趣的:(C/C++,c++,开发语言)