- c++关键字
- 命名空间
- c++输入/输出
- 缺省参数
- 函数重载
- 引用
- 内联函数
- auto关键字(c++11)
- 基于范围的for循环(c++11)
- 指针空值-nullptr(c++11)
在学习c++之前,需要先学习一些c++对于c设计上不合理的地方的优化,如作用域,指针,函数,宏等等
一.关键字
c++98关键字共有63个
c++11关键字为73个,新增的关键字:alignas,alignof,char16_t,char32_t,constexpr,nullptr,static_assert,thread_local,decltype,noexcept
二.命名空间
在同一个项目组,两个人有可能会取一个同一个变量名或函数名,c语言无法很好的解决命名冲突/命名污染问题,所以c++引入了命名空间的概念。std就是c++标准库的命名空间名,c++将标准库的定义和实现都放到了这个命名空间中
2.1命名空间的定义
命名空间的使用,需要借助namespace关键字,后面跟命名空间的名字(自定义),然后接{},{}内部的名字即为命名空间的成员。
- 命名空间可以嵌套定义
- 同一个工程中,可以有多个相同名字的命名空间,编译器最后会合在一个命名空间中
- 命名空间中可以定义变量,函数,类型
- 命名空间域只影响使用,不影响生命周期
//namespace 是关键字
//A是命名的名字
namespace A
{
int rand = 3;
}
namespace B
{
namespace C
{
int rand = 5;
}
int rand = 4;
}
2.2命名空间的使用
编译器找变量:先在局部找,找不到在全局找,如果全局找不到就报错。如果一个变量既在全局定义又在局部定义,要在局部访问全局变量就需要加作用域限定符
#include<iostream>
using namespace std;
int a = 2;
int main()
{
int a = 3;
//访问局部a,打印结果:3
cout << a << endl;
//访问全局a,打印结果:2
//左边空白代表直接从全局找,找不到报错
cout << ::a << endl;
return 0;
}
命名空间的使用三种方法:
1. **加命名空间名称以及作用域限定符**
我们可以将全局作用域看作一个命名空间,只是全局作用域的命名空间名字默认为空,如果我们想使用某个命名空间中的名字,只需要将左边的空白替换为该命名空间的名字
编译器直接从std命名空间中搜索
#include<iostream>
int main()
{
int a = 2;
//直接从A空间找,找不到报错,打印结果为3
std::cout << a << std::endl;
return 0;
}
2. **全部展开:使用using namespace 名字 引入,**
将std展开到全局,编译器搜索时,会到std命名空间搜索
#include<iostream>
using namespace std;
int main()
{
int a = 2;
//直接从A空间找,找不到报错,打印结果为3
cout << a << endl;
return 0;
}
3. **使用using将命名空间中的某个成员引入**
将cout,endl展开到全局作用域
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int a = 2;
//直接从A空间找,找不到报错,打印结果为3
cout << a << endl;
return 0;
}
三.c++输入/输出
- c++使用cout(标准输出对象,默认为控制台),cin(标准输入对象,默认为键盘),必须包含头文件iostream,以及使用命名空间std
- cout ,cin是全局的流对象,endl是特殊的c++符号,表示换行输出,它们都包含在iostream头文件中
- <<是流插入运算符,>>是流提取运算符
- cout 和 cin都可以自动识别对象类型
- cout是ostream类型的对象,cin是istream类型的对象
#include<iostream>
//在平常练习中,全局展开比较方便;但是项目中,建议展开成员
using namespace std;
int main()
{
int a = 3;
//自动识别类型
cin >> a;
cout << a << endl;
return 0;
}
四.缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
- 缺省参数不能在声明和定义同时出现
- 缺省参数分为全缺省和半缺省
- 全缺省
#include<iostream>
using namespace std;
void fun(int a = 3, int b = 3)
{}
int main()
{
//以下三种方法都可调用fun函数
fun();
fun(1);
fun(1, 2);
return 0;
}
- 半缺省
- 半缺省参数必须是从右向左连续给出的
#include<iostream>
using namespace std;
void fun(int a, int b = 3, int c = 2)
{}
int main()
{
fun(1);
return 0;
}
五.函数重载
5.1重载条件
对于c语言,不允许两个函数具有同一个函数名,原因是其函数名修饰规则不支持,而c++支持在同一个作用域下,声明多个功能类似的同名函数。这些函数的形参列表不同,如参数类型,个数,顺序。
- 返回值类型与函数重载无关
#include<iostream>
using namespace std;
int Add(int num1, int num2)
{
return num1 + num2;
}
double Add(doubel num1, double num2)
{
return num1 + num2;
}
void fun()
{
cout << endl;
}
void fun(int a)
{
cout << a << endl;
}
int main(void)
{
Add(2, 1);
Add(2.1, 2,1);
fun();
fun(1);
return 0;
}
5.2重载原理
代码经过:预处理,编译,汇编,链接形成可执行文件,在汇编时,会生成符号表,符号表里面存放有函数名以及其地址,编译器会根据函数调用处的函数名,找与其函数名相同的函数地址并且存放到符号表中,如果是多文件的话,会在链接时找,找不到会报链接错误。调用处的函数名和定义处的函数名是会被编译器修饰的。这就是函数名修饰规则。由于vs的函数名修饰规则比较复杂,所以使用Linux来观察函数名修饰规则。
下面是c++代码,使用g++编译
- objdump -S 可执行文件名 该命令可以查看信息
可以看到第一个函数名被修饰为:_Z3funi
第二个函数名被修饰为:_Z3funpi
第三个函数名被修饰为:_Z3funv
- 这里的_Z是前缀,一种格式
- 3是函数名的长度
- fun是函数名
- i是int ,pi 是int* , v是void
在函数调用处,编译器也会生成一个这样的修饰名,然后编译器会根据修饰名来找对应的重载函数,
这里也可以看出为什么返回值不能作为重载条件了,因为在函数调用处,不能确定返回值的类型。
而c语言的函数名修饰规则就简单:
这里c的函数名修饰规则就是函数名,所以我们也可以理解为什么c不支持函数重载了,因为c只会根据函数名找,而与参数无关,同名函数无法区分
六.引用
6.1引用概念
在语法层面,引用是给已存在的变量取了个别名,编译器不会为引用变量开辟空间,它和它引用的变量共用一个空间。
- 可以给变量的引用起别名
- 引用必须初始化
- 一个变量可以有多个引用
- 引用一旦初始化,不能在引用其他实体
#include<iostream>
using namespace std;
int main(void)
{
int a = 0;
int& b = a;
int& c = b;
int& d = a;
return 0;
}
6.2常引用
如果引用实体具有常量属性,那么我们使用引用时必须要加const。权限:读+写,常量属性只具有读权限,不具有写权限。如果我们不加const,那么引用就具有了写权限,这种叫做权限的放大,权限不允许放大,但是可以缩小或者平移
#include<iostream>
using namespace std;
int main()
{
const int a = 3;
//权限放大,会报错
//int& b = a;
const int& b = a;
//权限放大会报错
//int& c = 1;
const int& c = 1;
double d = 1.1;
//权限放大,此时会产生临时变量,用来保存类型转换为int的d的值,临时变量具有常性
//int& e = d;
const int& e = d;
return 0;
}
6.3使用
- 引用做输出型参数
#include<iostream>
using namespace std;
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main(void)
{
int num1 = 1;
int num2 = 2;
swap(num1, num2);
return 0;
}
- 做函数返回值
函数在返回变量时并不会直接返回,而会生成一个临时变量,如果占用空间少的话,就用寄存器来充当。如果大的话,会开辟一个临时变量来保存数据,临时变量具有常属性。
如果函数所返回的对象在函数销毁后仍然存在,则可以使用引用作为返回值。
好处:
- 可以减少拷贝
- 调用者可以修改返回对象
#include<iostream>
using namespace std;
/*
int Add(int a, int b)
{
int c = a + b;
return c; //不会直接将c返回,而是生成一个临时变量,保存c的值
}
int main()
{
//错误,因为临时变量具有常性
//int& ret = Add(1, 2);
const int& ret = Add(1, 2);
return 0;
}
*/
/*
int& Add(int a, int b)
{
int c = a + b;
return c; //返回值类型为引用,可以理解为返回了个c的别名,
}
int main()
{
//上述操作是未定义的,Add函数栈帧销毁后,c还给操作系统了,但是ret仍然引用c,
//此时ret的值是未定义的,具体看编译器是如何处理,
int& ret = Add(1, 2);
return 0;
}
*/
int& Add(int a, int b)
{
static int c = a + b;
return c; //由于c出来Add作用域后还在,因此不需要拷贝临时变量,用引用返回
}
int main()
{
int& ret = Add(1, 2);
return 0;
}
- 以值作为参数或者返回值类型,在传参或者返回期间,函数不会直接传递实参或者返回变量本身,而是传参或者拷贝一份临时变量,因此在传参或者值返回的效率相比引用低
6.4指针和引用的区别
- 从语法上,引用不用开辟空间和实体公用一块空间,而指针需要开辟空间来存放地址
- 引用必须初始化,指针不用初始化
- 没有空引用,但是有空指针
- 引用一旦指定实体,不允许在引用其他实体,而指针可以随意改变指向
- 没有多级引用,但是有多级指针
- sizeof引用大小为其实体类型的大小,sizeof指针,4或者8个字节
- 引用加1就是加1,但是指针加1,其加的是其指向类型的大小
- 引用直接使用就行,编译器会自己处理,指针还需要显示解引用
- 引用比指针更安全
七.内联函数
c++极其不推荐使用宏:常量宏,函数宏
宏的优点:
- 提高代码复用性
- 提高性能
宏的缺点:
- 不可调试
- 无类型检查,不够严谨
- 不可递归
- 有优先级问题
- 可能会增大代码量
c++推荐使用const,enum来取代常量宏,推荐使用inline来取代函数宏
7.1内联函数概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
- 内联函数声明和定义不建议分文件,如果分离会导致链接错误,只有声明,所以编译器会生成call指令,在链接阶段去找对应的函数地址,但是inline函数并不会进入符号表,所以会导致链接错误
- 内联函数只是一种建议,编译器可以采用也可以不用
- 如果编译器将函数当作内联函数,则会在编译阶段用函数体代替函数调用语句
八.auto关键字(c++11)
在代码中,有可能会遇到很长很难写的类型,这时可以用typedef重定义。但是typedef有一个缺点:
#include<iostream>
using namespace std;
typedef char* pstr;
int main(void)
{
//下面变量定义哪个会出错?
//const pstr p1; //该变量定义会出错,因为const修饰p1,必须初始化
const pstr p1 = nullptr;
const pstr* p2;
cout << typeid(p1).name() << endl;
cout << typeid(p2).name() << endl;
return 0;
}
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义
8.1auto性质
早期c/c++中,auto是定义自动存储器的局部变量。c++11中,auto是一个新的类型提示符来指示编译器,auto声明的变量类型是由编译器推导而来的。
- auto变量必须初始化,编译器会在编译期间将auto替换为实际类型。
- auto可以与指针或者引用结合使用,使用auto声明引用类型时,必须加&,声明指针时都一样
- 使用auto在同一行声明多个变量时,变量类型必须一致
- auto不能作为函数参数,因为编译器不能推导形参的实际类型
- auto不能声明数组
九.范围for(c++11)
c++11新增了一种遍历数组的方法:
#include<iostream>
using namespace std;
//注意:下面函数的范围for是错误的,因为num实际上是一个指针,不是一个数组
void fun(int num[])
{
for (auto e : num)
{
cout << e << ' ';
}
}
int main(void)
{
int num[] = {1, 3, 5, 2, 6};
// auto 也可以换为int
// e : 也可以随意换名字
for (auto e : num)
{
cout << e << ' ';
}
cout << endl;
for (auto& e : num)
{
e *= 2;
cout << e << ' ';
}
cout << endl;
return 0;
}
十.nullptr关键字(c++11)
在c中,NULL是一个宏,NULL的实现如下:可以看到,在c++中,NULL变成了一个int,有些场景会出错,因此c++11,打了个补丁,新增了nullptr作为指针空值,所以以后最好使用nullptr
/*#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif */
//下面这种场景,NULL会出错
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL); //实际上等价于f(0)
f((int*)NULL);
return 0;
}