前言
此篇,就正式进入C++的学习了。
C++,高级语言,从名字也能窥见一二,是C语言的 “升级版”。它对C语言中许多空白的地方进行填补,不足的地方进行优化,允许我们 “面向对象编程”。因此我们在学习过程中可以多多对比C++和C。它的优势也和C一样,可以直接操控硬件,相比其他高级语言较底层,因此适合做游戏开发、服务器开发等等偏后端的事儿。
今天是C++学习的第一站,我们先来学学基础语法第一课——C++入门
主要内容有:
- 命名空间
- 缺省参数
- 函数重载
- 引用
- 内联函数
- auto
- 范围for
- nullptr
1. 命名空间
C的命名缺陷
C语言中关于命名,同个域中不允许变量、类型同名,也不允许函数同名。这会带来一些问题
- 自己定义的变量、函数,可能会和库里的冲突
- 做大项目的时候,也常出现命名冲突问题
#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);
-
命名空间的使用分以下三种:
-
使用时指定成员
printf("%d", bacon :: pig); :10
隔离效果最佳,但是使用较麻烦
-
引入指定成员
using bacon :: pig; printf("%d", pig); :10
常用的可以引入
-
引入整个命名空间
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 操作
引用作参数的优势:
- 减少拷贝,提高效率
- 引用参数是输出型参数——函数中修改形参,实参也跟着改变
作返回值
// 传值返回:返回时创建一个临时变量,先把返回值放到临时变量中,再通过临时变量把返回值传递给调用者
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 微秒
引用返回的优势:
- 减少拷贝, 提高效率
- 可以对返回值操作
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:类似赋值操作
二者都是
- 把 [i] 的有效地址 加载到寄存器 eax 中
- 把 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,期待与你共同进步!