【C++初阶1-C++入门】这么多新增的语言特性,超好用!

前言

此篇,就正式进入C++的学习了。

C++,高级语言,从名字也能窥见一二,是C语言的 “升级版”。它对C语言中许多空白的地方进行填补,不足的地方进行优化,允许我们 “面向对象编程”。因此我们在学习过程中可以多多对比C++和C。它的优势也和C一样,可以直接操控硬件,相比其他高级语言较底层,因此适合做游戏开发、服务器开发等等偏后端的事儿。

今天是C++学习的第一站,我们先来学学基础语法第一课——C++入门

主要内容有:

  • 命名空间
  • 缺省参数
  • 函数重载
  • 引用
  • 内联函数
  • auto
  • 范围for
  • nullptr

1. 命名空间

C的命名缺陷

C语言中关于命名,同个域中不允许变量、类型同名,也不允许函数同名。这会带来一些问题

  1. 自己定义的变量、函数,可能会和库里的冲突
  2. 做大项目的时候,也常出现命名冲突问题
#include <stdio.h>

int rand = 10;

int main()
{
	printf("%d\n", rand);

	return 0;
}

:10

现在可以正常输出,那我这样呢?

#include <stdio.h>
#include <stdlib.h>

int rand = 10;

int main()
{
	printf("%d\n", rand);

	return 0;
}

在这里插入图片描述

此处就是自己定义的变量和库里的冲突,说到这,我们再问一个问题:

问:编译器是怎么找变量的?
#include <stdio.h>
//#include <stdlib.h>

int rand = 10;

int main()
{
	int rand = 20;

	printf("%d\n", rand);

	return 0;
}

:20

局部 ==> 全局 ==> 找不到则报错

C++为了解决这个问题,提出了命名空间的概念…

命名空间

namespace 命名空间是C++内置的关键字,允许人为创建一个域、逻辑分组。可以隔离开不同命名空间下的符号(函数、全局变量等)
创建一个命名空间就像创建一个文件夹:A文件夹下有一个 add.h,不影响B文件夹下有一个 add.h。

命名空间的定义

先看看它用起来是什么样的

#include <stdio.h>

namespace bacon
{
	int pig = 10;
}

int main()
{
	printf("%d\n", bacon::pig);
}

:10

语法:namespace [命名空间名]

有点像结构体啊,是的,结构体和命名空间的本质都是封装,只不过结构体封装的是类型,命名空间封装的是“名字”,它的本质是:

  • 隔离 全局变量、类型、函数

  • 并改变编译器查找规则

改变编译器查找规则?代码里的 : : 是什么东西?

命名空间的使用

域作用限定符

语法:mynamespace : : numbers

​ 命名空间 : : 成员

上面的代码中, bacon : : pig 的含义就是,在命名空间bacon中查找pig成员,这也是“改变编译器查找规则”的意思

编译器查找规则
  • 不指定命名空间:局部 ==> 全局 ==> 报错(不指定bacon,编译器查找的时候就会当作没看到bacon)

  • 指定命名空间

    • 在命名空间找

      printf("%d\n", bacon :: pig);
      
    • 在全局找

      printf("%d\n", :: pig);
      

命名空间的使用分以下三种:

  1. 使用时指定成员

    printf("%d", bacon :: pig);
    
    :10
    

    隔离效果最佳,但是使用较麻烦

  2. 引入指定成员

    using bacon :: pig;
    
    printf("%d", pig);
    
    :10
    

    常用的可以引入

  3. 引入整个命名空间

    using namespace bacon;
    
    printf("%d ", pig);
    printf("%s", song);
    
    :10 Mojito
    

    隔离失效,平时练习可以这么用,做项目之类的就别了,会造成命名污染

命名空间的嵌套定义

namespace bacon
{
	int pig = 10;

	namespace idol
	{
		namespace jay
		{
			const char* song = "Mojito";
		}
	}
}

int main()
{
	printf("%s\n", bacon::idol::jay::song);
}

:Mojito

总结

  • 是什么:对命名的封装
  • 为什么:防止命名冲突和污染
  • 怎么用:使用时指定、引入某一成员和引入某一命名空间

2. hello world

了解完命名空间,我们进一步学习 hello world 程序

#include <iostream>

using namespace std;

int main()
{
	cout << "hello world" << endl;
	
	return 0;
}

:hello world
  • #include <iostream> :告诉编译器我们要用 iostream 这个头文件

    • cin:标准输入
    • cout:标准输出
    • *cerr:标准错误
    • *clog:输出程序运行时的一般信息
    • << :流插入运算符
    • >>:流提取运算符
  • using namespace std:std,C++标准库的命名空间名,标准库的定义实现都在这个命名空间中

*c++中没有定义输入输出的语句,而是包含了一个全面的标准库来提供IO机制。iostream库包含两个基础类型 istream 和 ostream 表示输入输出流。一个流就是一个字符序列,“流”想表达的是,随着时间推移,字符序列顺序生成或消耗

问:那想控制输出格式呢?

用printf方便就用呗!哪个方便用哪个


3. 缺省参数(默认参数)

缺省参数,就是在声明函数的某个参数的时候,为之指定一个默认值,在调用该函数的时候如果不给缺省参数传参,会采用其默认值。

缺省参数也分 全缺省 和 半缺省:

全缺省

#include <iostream>

using namespace std;

int f1(int e1 = 10, int e2 = 20)
{
	return e1 + e2;
}

int main()
{
	int ret = f1();

	cout << ret << endl;

	return 0;
}

:30

半缺省:只能从右往左连续缺省(需要缺省的往后放)

int f2(int e1, int e2 = 20)
{
	return e1 + e2;
}

int main()
{
	int ret = f2(100);

	cout << ret << endl;

	return 0;
}

:120

注意:

  • 缺省参数不能在函数的声明和定义中同时出现(我们放在声明里就好)

    • 如果声明定义的缺省值不同,编译器不知道用哪个了
  • 缺省参数一般是 常量 或 全局变量

总结

  • 是什么:具有默认值的参数
  • 怎么用:全缺省或半缺省(从左到右连续缺省)

4. 函数重载

函数重载(fuction overload),指 我们可以通过传不同的参数来区分同名的函数。同一作用域下允许同名不同参数列表的函数同时存在。

构成函数重载的条件:同一作用域下,函数名相同 + 参数列表不同(类型、个数、顺序)。

#include <iostream>

using namespace std;

int Add(int e1, int e2)
{
	return e1 + e2;
}

double Add(double e1, double e2)
{
	return e1 + e2;
}

int main()
{
	int i1 = 10;
	int i2 = 20;
	double d1 = 1.1;
	double d2 = 2.2;

	int reti = Add(i1, i2);
	double retd = Add(d1, d2);

	cout << reti << endl;
	cout << retd << endl;

	return 0;
}30
3.3

cin/cout 的自动识别类型也是函数重载,通过参数列表区分不同的输出类型

在这里插入图片描述

函数重载的二义性

现有两个构成重载的函数,调用时我们传的参数无法让编译器明确区分两个函数,我们称这两个构成重载的函数有 二义性

#include <iostream>

using namespace std;

void f()
{
	cout << "f()" << endl;
}

void f(int e1 = 0, int e2 = 0)
{
	cout << "f(int e1 = 0, int e2 = 0)" << endl;
}

int main()
{

	return 0;
}

不调用并编译,能编译成功,说明构成重载

现在来调用一下看看

#include <iostream>

using namespace std;

void f()
{
	cout << "f()" << endl;
}

void f(int e1 = 0, int e2 = 0)
{
	cout << "f(int e1 = 0, int e2 = 0)" << endl;
}

int main()
{
	f();

	return 0;
}

在这里插入图片描述

实际使用中要注意避免产生二义性

函数重载的简单原理

函数重载的原理不适合现阶段解剖,我们浅浅了解一下就够

我们知道,函数调用就是找到函数名对应的地址,执行地址处的函数体,但,相同的函数名怎么找到不同的地址?

虽然C++中构成函数重载的函数名看起来一样,但其实不尽相同…

函数名修饰

C++就是通过函数名修饰规则来实现函数重载的。

(由于win的vs下函数名修饰太复杂,Linux下的gcc/g++就很好理解,所以下面用gcc演示)

int Add(int a, int b)
{
	return a + b;
}

void func(int a, double b, int* p)
{}

int main()
{
	Add(1, 2);
	func(1, 2, 0);
	return 0;
}
  • C语言编译器

在这里插入图片描述

  • C++ 编译器

在这里插入图片描述

可以看到,C编译器编译后,函数名不变;C++编译器编译后函数名被修饰了:[ _Z + 函数名长度 + 函数名 + 参数类型首字母 ]

我们使两个函数构成重载时,参数列表的不同,已经决定了两个函数是“不一样的函数”了,怎么说?

不同的参数列表,使函数名被修饰后成了不同符号。这样一来,编译器 就能够分辨构成重载的同名函数谁是谁了。

问:返回值类型不同能不能构成函数重载?

不能的话,那我们在函数名修饰中带上返回值类型不就好了吗?

:不能构成,而且也并不是因为函数名修饰,而是 调用时的二义性——

调用的地方不能控制返回值的类型,我调用返回值为int的,你给我调用返回值为char的,那可不行

总结

  • 是什么:参数列表不同的同名函数,能执行不同代码
    • 原理是C++生成符号的时候,会把函数的参数列表修饰进函数名,产生带有参数列表标识的函数符号
  • 避免二义性:
  • 返回值必须相同 —— 不同的返回值产生二义性
  • 参数列表不能有二义性如 —— 全缺省 和 无参数 的函数构成重载,调用时就会有二义性

5. 引用

引用,一种数据类型,像是变量的同位语,或是给它取别名,一块空间的不同名字

比如: 周杰伦,JayChou,很多人的青春…

周杰伦就是JayChou,JayChou就是很多人的青春,没有区别

#include <iostream>

using namespace std;


int main()
{
	int i = 10;
	int& ri = i;
	int& rri = i;

	cout << &i << endl;
	cout << &ri << endl;
	cout << &rri << endl;

	return 0;
}

:02D8FBE0
02D8FBE0
02D8FBE0

特性:

  • 一个实体可以有多个引用
  • 引用必须初始化,且初始化后不能更改引用的实体
    • 所以C++的引用代替不了指针,二者相辅相成
#include <iostream>

using namespace std;

int main()
{
	int i1 = 10;
	int i2 = 20;
	int& ri = i1;

	ri = i2; // 此时的 ri 就等同于 i1,所以这里是赋值操作,不是引用新实体

	cout << ri << endl;

	return 0;
}

:20

引用的应用场景

作参数

#include <iostream>

using namespace std;

int f(int& ri)
{
	return ri *= 10;
}

int main()
{
	int i = 10;
	
	int ret = f(i);

	cout << ret << endl;

	return 0;
}

ri 接收到参数 i 后就成了 i 的别名,对 ri 操作 == 对 i 操作

引用作参数的优势:

  1. 减少拷贝,提高效率
  2. 引用参数是输出型参数——函数中修改形参,实参也跟着改变

作返回值

// 传值返回:返回时创建一个临时变量,先把返回值放到临时变量中,再通过临时变量把返回值传递给调用者
int f1() {
	int num = 1;
	num *= 10;
	return num;
}

//引用返回:返回时直接返回变量的引用(地址) -- 无需创建临时变量,也减少了一次拷贝(返回值->临时变量)
int &f2(int &num) {
	num *= 10;
	return num;
}

先回忆一下 传值返回…

传值返回

:创建一个和返回值类型同类型的临时变量(之后称作tmp)==> 将 n1 放进 tmp ==> 销毁栈帧 ==> 通过临时变量带回返回值

这样一来就有一层 拷贝

// 函数准备返回
int tmp = n1;
// 函数返回,销毁栈帧
调用方接收返回值的变量 = tmp;

(tmp小就创建在寄存器内,大就创建在上一层栈帧)

需要十分注意的是:临时变量具有常性!!

临时变量产生场景:

  • 类型转换(强转,提升和截断)
  • 传值返回
问:不能直接返回n1吗,为什么要用临时变量帮助返回?

n1是开辟在栈上的临时变量,出作用域就销毁,数据不由我们控制了

引用返回

:相比传值返回,引用返回会直接返回引用,无需创建临时变量并拷贝

注意:不要返回局部变量的引用,否则函数返回后函数栈帧销毁,其局部变量的值是未定义的,此时它的引用就变成了 悬空引用

int &f() {
	int n = 1;
	return n; // 返回局部变量的引用,函数栈帧销毁后,成为 悬空引用!
}

一个小程序对比时间:

#include <chrono>
#include <vector>

using namespace std::chrono;

// 传入一个函数和参数,返回函数执行时间
template<typename F, typename... Args>
auto measure_execution_time(F &&func, Args&&... args) {
	auto start = high_resolution_clock::now();
	forward<decltype(func)>(func)(forward<Args>(args)...);
	auto end = high_resolution_clock::now();
	return duration_cast<microseconds>(end - start);
}

// 大型对象类
class LargeObject {
private:
    std::vector<int> data;
public:
    // 构造函数,初始化大量数据
    LargeObject() {
        // 假设我们需要初始化一个包含 1000000 个随机整数的向量
        for (int i = 0; i < 1000000; ++i) {
            data.push_back(rand());
        }
    }

    // 传值返回函数
    LargeObject getValueReturn() {
        return *this; // 返回大对象的副本
    }

    // 传引用返回函数
    LargeObject &getReferenceReturn() {
        return *this; // 返回大对象的引用
    }
};

int main() {
	int num = 2;
    LargeObject largeObject;

    // 计算传值返回函数的执行时间
    auto startValueReturn = high_resolution_clock::now();
    LargeObject copyObject = largeObject.getValueReturn();
    auto endValueReturn = high_resolution_clock::now();
    auto durationValueReturn = duration_cast<microseconds>(endValueReturn - startValueReturn);

    cout << "传值返回函数执行时间: " << durationValueReturn.count() << " 微秒" << endl;

    // 计算传引用返回函数的执行时间
    auto startReferenceReturn = high_resolution_clock::now();
    LargeObject &refObject = largeObject.getReferenceReturn();
    auto endReferenceReturn = high_resolution_clock::now();
    auto durationReferenceReturn = duration_cast<microseconds>(endReferenceReturn - startReferenceReturn);

    cout << "传引用返回函数执行时间: " << durationReferenceReturn.count() << " 微秒" << endl;
	
	return 0;
}
传值返回函数执行时间: 1108 微秒
传引用返回函数执行时间: 0 微秒

引用返回的优势:

  1. 减少拷贝, 提高效率
  2. 可以对返回值操作

const 引用

const引用,不能被修改。

权限原则:对指针和引用的赋值中,权限可以平移或缩小,但是不能放大
比如:

  • 本来可以修改的对象,可以赋值给普通指针或引用(平移)
  • 本来可以修改的对象,可以赋值给const指针或引用(缩小)
  • 本来不能修改的对象,只能赋值给const指针或引用(平移)
  • 本来不能修改的对象,不能赋值给 普通指针或引用(放大) – 本来就不能修改,如果允许权限放大的引用,那我被引用的对象就危险了
int main() {
	int a = 10;
	const int b = 20;

	// 本来可以修改的对象,可以赋值给普通指针或引用(平移)
	int *pa = &a;
	int &ra = a;

	// 本来可以修改的对象,可以赋值给const指针或引用(缩小)
	const int *pa = &a;
	const int &ra = a;

	// 本来不能修改的对象,只能赋值给const指针或引用(平移)
	const int *pb = &b;
	const int &rb = b;

	// 本来不能修改的对象,不能赋值给 普通指针或引用(放大) -- 本来就不能修改,如果允许权限放大的引用,那我被引用的对象就危险了
	int *pb = &b; // error
	const int &rb = b; // error

	return 0;
}

在这里插入图片描述

问:为了减少拷贝提高效率,把函数的参数都设计成引用,好么?

不好,传参会受限制,可能会权限放大——比如形参是可读可写的引用,实参传了只读的实体

那怎么做?

const引用不就来了嘛,函数内不需要修改的形参,都设计成const引用 – 你不管传什么,都是 权限缩小或平移

#include <iostream>
using namespace std;

int f(const int& e1)
{
    //...
}

int main()
{
	int a = 10;//缩小
	const int b = 20;//平移

	f(a);
	f(b);

	return 0;
}

引用的实现

既然它和指针这么像,我们就好好看看它们到底有什么区别,上反汇编…

在这里插入图片描述

lea(load effective address):加载有效地址

mov:类似赋值操作

二者都是

  1. 把 [i] 的有效地址 加载到寄存器 eax 中
  2. 把 eax 中的地址赋给 [ra]/[pa]

可知,引用底层实现上是占空间的,也可知,指针和引用的底层实现是一样的,相当于一个自动解引用的指针;但是语法上,引用还是不占空间。

总结

  • 是什么:变量的别名
  • 为什么:有些场景比指针更适合
  • 作用
    • 做参数:减少拷贝 + 输出型参数
    • 做返回值:减少拷贝 + 可对返回值操作
  • const引用
    • 核心:指针和引用的运算中,权限可以缩小、平移,不能放大
  • 实现:“自动解引用的指针”

6. 内联函数

C语言中,对于规模小、结构简单、重复调用的小函数,总是开辟栈帧开辟栈帧,很浪费性能,C++提出内联函数来解决这个问题

内联 inline

inline是C++中的关键字,只是建议编译器将某函数视为内联函数,编译器会根据情况来决定

特性:

1. 内联函数在预处理后会在调用函数的地方直接展开,不会开辟栈帧,而是直接执行指令

*也代表如果内联函数的规模大/结构复杂,会造成代码膨胀:比如递归,疯狂地展开一堆代码,最直接的体现就是最终出来的exe文件会变得很大

2. 不适合声明定义分离
直接展开,代表内联函数没有函数地址,不会进符号表,链接时会产生链接错误

// 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()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

应用:小函数

  • 规模小
  • 结构简单
  • 重复调用

本质上,inline是一种空间换时间的做法

总结

  • 是什么:预处理后会在调用处直接展开函数题的函数
  • 为什么:对小函数频繁调用,开辟栈帧效率不高
  • 怎么用:对规模小、结构简单和调用频繁的函数使用

面试题

问:宏的优缺点?

优点:

  • 提高代码 复用性
  • 提高性能

缺点:

  • 可读性差,可维护性差,易错
  • 没有类型安全的检查
  • 无法调试

问:如何解决宏的缺点?

1. 换用const enum来定义常量
2. inline 小函数


7. auto(C++11)

auto,一种数据类型,可以根据被赋的值自动推导类型,是C++的关键字

为什么会出现这样的数据类型?学到后面,会发现类型的命名简直复杂:

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

std::map<std::string, std::string>::iterator 是一个类型,也许可以typedef?

typedef char* pstring;
int main()
{
	const pstring p1; 
	const pstring* p2;
	return 0;
}

typedef要求我们 声明变量的时候必须知道之后会给它赋什么类型的值,有时候很苦恼, 那看看auto

std::map<std::string, std::string>::iterator it = m.begin();

auto it = m.begin

舒服

int main()
{
	int x = 1;

	auto a1 = x;//自动识别
	auto a2 = &x;//自动识别
	auto* a3 = &x;//指定auto为指针类型
	auto& a4 = x;//指定auto为引用类型

	//typeid(variable).name():拿到变量类型名称的字符串
	cout << typeid(a1).name() << endl;
	cout << typeid(a2).name() << endl;
	cout << typeid(a3).name() << endl;
	cout << typeid(a4).name() << endl;

	return 0;
}

:int
int *
int *
int //语法上来  说,x是int类型,它的别名找到的一小块内存空间也是int类型

使用注意事项

1. auto声明引用类型时,必须指定auto为引用类型(否则会直接识别为原类型)

int main()
{
	int x = 1;

	//auto声明/定义指针类型,指不指定都行
	auto a1 = &x;//自动识别
	auto* a2 = &x;//指定auto为指针类型

	//auto声明/定义引用类型,必须指定
	auto a3 = x;//自动识别为int了
	auto& a4 = x;//指定auto为引用类型

	cout << typeid(a1).name() << endl;
	cout << typeid(a2).name() << endl;
	cout << typeid(a3).name() << endl;
	cout << typeid(a4).name() << endl;

	*a1 = 10;
	cout << x << endl;

	a3 = 20;//a3与x无关
	cout << x << endl;

	a4 = 30;
	cout << x << endl;

	return 0;
}

2. 一行声明多个auto变量时,整行的变量类型必须相同(auto根据第一个变量类型,确定后续的类型)

int main()
{
	auto a1 = 1234, a2 = 123.4;
	return 0;
}

在这里插入图片描述

3. auto不能作为函数参数

函数开辟栈帧前,要先根据参数计算要开辟多大空间,但是auto大小不确定使得编译器无法计算

void TestAuto(auto a)
{}

在这里插入图片描述

4. auto不能直接声明数组

int main()
{
	auto a[] = { 1, 2, 3, 4 };
	return 0;
}

在这里插入图片描述

5. 为了不和C++98的auto混淆,C++11的auto只作为类型指示符

总结

  • 是什么:会自动识别类型的类型
  • 为什么:有些类型名冗长
  • 怎么用:auto i = XXX;
    • 引用需要指定
    • 一行声明多个,需要整行类型相同(只识别第一个)
    • 不能作为函数参数(无法确定函数栈帧大小)
    • 不能直接声明数组

8. 基于范围的 for

范围for,自动判断范围,自动迭代

使用时,需要给一个迭代变量,再给定迭代范围,每次迭代都重新创建迭代变量

  • 迭代变量:可以直接用auto,方便
  • 迭代范围:必须是确定的
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	for (auto e : arr)//迭代变量叫啥都可以,e代表element
		cout << e << ' ';
	cout << endl;

	return 0;
}

:1 2 3 4 5 6 7 8 9 10

本质就是每次取迭代范围中的数据赋值给迭代变量。

使用注意事项

1. 迭代时,编译器每次取范围内的数据,赋值给迭代变量,迭代变量并不改变范围内数据

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	for (auto e : arr)
		e *= 2;

	for (auto e : arr)
		cout << e << ' ';
	cout << endl;

	return 0;
}

:1 2 3 4 5 6 7 8 9 10

如果想改变,设计成 auto& e

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	for (auto& e : arr)
		e *= 2;

	for (auto e : arr)
		cout << e << ' ';
	cout << endl;

	return 0;
}

:2 4 6 8 10 12 14 16 18 20

用auto*可以吗?不行,范围for只是每次取 arr[0]、arr[1],是int类型,不能用int*接收(引用还是有意义滴)

2. 迭代范围必须是确定的

如果迭代数组,范围就是第一个元素到最后一个元素
如果迭代类,要提供begin( )、 end( ) 两个方法

//数组传参后变指针,范围不确定了
void TestRangeFor(int[] arr)
{
	for (auto e : arr)
		cout << e << endl;
}

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	TestRangeFor(arr);

	return 0;
}

3. 迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)

总结

  • 是什么:自动迭代的循环
  • 为什么:写起来方便
  • 怎么用:for(迭代变量 : 迭代范围)
    • 对数组,范围必须确定
    • 对自定义类型,必须提供begin和end方法

9. nullptr

学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h )中的定义是这样:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	return 0;
}

:f(int)
f(int)
f(int*)

我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?

语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:

用 nullptr 来代替 NULL 的功能

nullptr 就相当于 (void*)0

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(nullptr);
	return 0;
}

:f(int)
f(int*)

注意:

  • nullptr 是关键字
  • C++11中,sizeof(nullptr) 和 sizeof((void*)0) 相等
  • 后续最好使用 nullptr

练习

下面关于C++命名空间描述错误的是( )
A.命名空间定义了一个新的作用域。
B.std是C++标准库的命名空间。
C.在C++程序中,命名空间必须写成using namespace std; (理解为:用 std 中的成员,必须展开整个 std 命名空间)
D.我们可以自己定义命名空间。

A 是的,命名空间定义了一个新的作用域
B 是的,std是C++标准库的命名空间
C 错误,可以使用时某成员时指定命名空间,也可以直接引入命名空间中某个成员到程序中,也能直接展开整个命名空间。三种方法都行,而不是必须通过 using namespace std 展开整个命名空间
D 是的,我们可以自己定义命名空间

下面关于C++缺省参数描述错误的是( ) 【不定项缺省】

作业内容
A.缺省参数是声明或定义函数时为函数的参数指定一个默认值.
B.在调用有缺省参数的函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
C.C和C++都支持缺省参数
D.全缺省就是参数全部给缺省值,半缺省就是缺省一半的值

关于引用以下说法错误的是( )。(阿里巴巴2015笔试题)
A.引用必须初始化,指针不必
B.引用初始化以后不能被改变,指针可以改变所指的对象
C.不存在指向空值的引用,但是存在指向空值的指针
D.一个引用可以看作是某个变量的一个“别名” E.引用传值,指针传地址 F.函数参数可以声明为引用或指针类型

解析:
A 是的,引用必须初始化,指针可以不初始化
B 是的,准确的说是引用初始化以后不能被改变引用的对象,指针可以改变所指的对象
C 是的,空值不是具体的对象,无法被引用,而指向空值的指针就是我们说的空指针啦
D 是的,一个引用可以看作是某个变量的一个“别名”
E 错误,引用表面上是传值,底层实际是传地址;指针确实是传地址
F 是的,函数参数可以声明为引用或指针类型
选E。

以下不是double compare(int,int)的重载函数的是( )
A.int compare(double,double)
B.double compare(double,double)
C.double compare(double,int)
D.int compare(int,int)

解析:
函数重载:同一作用域下,函数名相同,参数列表不同(类型、个数、顺序),和返回值没有关系。
题目中的函数:double compare(int,int)
A int compare(double,double),同名,且参数列表不同,和题目中的函数构成重载
B double compare(double,double),同名,且参数列表不同,和题目中的函数构成重载
C double compare(double,int),同名,且参数列表不同,和题目中的函数构成重载
D int compare(int,int),同名,参数列表也相同,无法和题目中的函数构成重载
选D。

“引用”与指针的区别是什么( )
A.指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作
B.引用通过某个引用变量指向一个对象后,对它所指向的变量间接操作。程序中使用引用,程序的可读性差;而指针本身就是目标变量的别名,对指针的操作就是对目标变量的操作
C.指针比引用更节省存储空间
D.以上都不正确

解析:
题目显然是从宏观概念的角度看,而非底层实现的角度。
概念上来说:

  • 指针保存的是对象的地址,操作的时候解引用才能操作,算是间接
  • 引用就是作为对象的别名,操作引用的时候就等于操作对象,算是直接
    选A。

关于引用与指针的区别,下面叙述错误的是( )
A.引用必须被初始化,指针不必
B.指针初始化以后不能被改变,引用可以改变所指的对象
C.删除空指针是无害的,不能删除引用
D.不存在指向空值的引用,但是存在指向空值的指针

解析:
A 是的,引用必须被初始化,指针不必
B 错误,指针初始化以后可以被改变,引用则很专一,不能改变所指的对象
C 是的,空指针没有任何指向,删除无害;而引用是对象的别名,删除引用就等同于删除真实对象,有影响(按题目的说法就是有害)
D 是的,不存在指向空值的引用,但是存在指向空值的指针


今天的分享就到这里,感谢大家能看到这,不足之处多多交流

这里是培根的blog,期待与你共同进步!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值