目录
1. C++关键字
2. 命名空间
1)命名空间的引入和概述
2)命名空间的定义
3)std与命名空间的使用
4).相关特性
3. C++输入&输出
4. 缺省参数
1 )缺省参数概念
2)使用及分类
a.全缺省
b.部分缺省
5. 函数重载
1) 函数重载概念
2)分类
3)原理
6. 引用
1)引用概念和使用
2)引用特性
3)常引用
4)使用场景
5)引用与指针
7. 内联函数
1)概念
2)特性
8. auto关键字(C++11)
1)概念和使用
2)特性
9. 基于范围的for循环(C++11)
1 )范围for的语法
2) 范围for的使用条件
10. 指针空值---nullptr(C++11)
1)NULL
2)nullptr
1. C++关键字
C++总计63个关键字,C语言32个关键字
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. 命名空间
1)命名空间的引入和概述
在c++中,符号常量、变量、函数、结构、枚举、类和对象等名称将都存在于全局作用域中,用户的命名与这些名称相同而冲突的可能性非常大。并且工程越大,互相冲突性的可能性越大。另外使用标准类库时,也可能导致命名冲突。为了避免这些情况,C++引入了关键字namespace(命名空间/名字空间/名称空间)来控制标识符的作用域从而达到目的。
例如,在c里面:
int a=1;
void f1()
{
int a=2;
//可以取名相同,因为域不同
}
void f2()
{
int a=3;
}
2)命名空间的定义
定义命名空间,需要使用到namespace关键字,后面加命名空间的名字,然后接一对{}即可,{}
中即为命名空间的成员。
//分别定义名字为A,B的命名空间
namespace A {
int a = 10;
void print()
{
cout << "A:print" << endl;
}
}
namespace B {
int a = 20;
void print()
{
cout << "A:print" << endl;
}
}
void test1()
{
cout << "A中a = " << A::a << endl;
cout << "B中a = " << B::a << endl;
}
int main()
{
test1();
A::print();
B::print();
}
编译器默认只在全局域里查找相关变量函数等,不会进入特定的命名空间里查找。一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。需要使用特定的命名空间里的变量等,需要指定命名空间,指定命名空间需要使用"::"。
"::"是作用域限定符或者称作用域运算符(scope operator),
用法:namespace::name
3)std与命名空间的使用
std命名空间是C++中标准库类型对象的命名空间。如需要使用cout等就有多种方法。
a. using namespce std; -->代表把整体std展开,此时应该避免与sd里的名称冲突。
编译器此时会去std里面查找相关内容。
b. 所有使用都用"::"指定。
c. 只把部分常用的展开,已经展开的不需要用"::"。
4).相关特性
a. 命名空间只能全局范围内定义
b. 命名空间可以嵌套
namespace A
{
int a = 0;
namespace B
{
int a = 100;
}
}
c. 随时把新的成员加入已有的命名空间中
namespace A
{
int a = 0;
}
namespace A
{
int b = 100;
}
int main()
{
cout << A::a << endl << A::b << endl;
}
所以当项目里面有两个命名空间相同时,会被合并。
d. 命名空间中的函数可以在“命名空间”外定义
namespace A
{
int a = 0;
void A_fun();
}
void A::A_fun()
{
cout << " hello " << endl;
}
int main()
{
A::A_fun();
}
3. C++输入&输出
#include<iostream>
using namespace std;
int main()
{
int d;
cin >> d;
cout << d << endl;
}
1)在C++里, 如果需要使用输入输出时,必须包含< iostream >头文件,它包含了用于输入输出的对象, cout和cin是全局的流对象,cin
表示标准输入、cout
表示标准输出。endl是特殊的C++符号,表示换行输出。
2) <<是流插入运算符,>>是流提取运算符。
3)使用C++输入输出不需要手动控制格式,自动识别变量类型。
4) 实际上cout和cin分别是ostream和istream类型的对象,>>和<<是运算符重载。
注:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应
头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,
规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因
此推荐使用<iostream>+std的方式。
4. 缺省参数
1 )缺省参数概念
声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void fun1(int a = 100)
{
cout << "a的值:" << a << endl;
}
int main()
{
fun1();
fun1(1);
fun1(5);
}
2)使用及分类
a.全缺省
void fun(int a = 1, int b = 2, int c = 2)
{
cout << a << b << c << endl;
}
int main()
{
fun();
fun(12);
fun(10,20);
}
b.部分缺省
void fun(int a , int b = 2, int c = 2)
{
cout << a << b << c << endl;
}
int main()
{
fun(12);
fun(10,20);
fun(10, 20, 30);
//fun(10, , 30);
}
注意:
!缺省参数不能在函数声明和定义中同时出现,一般都在声明中给出。
!缺省值必须是常量或者全局变量
5. 函数重载
1) 函数重载概念
在开发中,有时候我们可能需要实现几个功能类似的函数,只是有些细节不同。例如希望交换两个变量的值,两个变量有多种类型,可以是 int、float、char、bool 等,在C语言中,只能分别设计出各个不同名的函数。
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
void swap(int* a, int* b)
{
int ret = *a;
*a = *b;
*b = ret;
}
void swap(double* a, double* b)
{
double ret = *a;
*a = *b;
*b = ret;
}
2)分类
a、参数类型不同
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
b、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f()" << endl;
}
c、参数类型顺序不同
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;
}
注意:函数的返回值不同时不能完成函数重载。
3)原理
①为什么C语言不支持函数重载?
程序在预编译阶段会经历预处理、编译、汇编和链接生成可执行程序的过程。
首先在汇编过程中编译器对每个文件都会收集全局符号并生成全局符号如果暂时找不到函数的地址(例如只有函数的声明),符号表里将会填写一个没有意义的值:
生成了多个符号表后,在链接过程中要对符号表进行合并。在合并的过程中如果函数出现了两次,——函数有效的地址值就会作为Add函数最终的地址值。C语言中因为两个重名函数的地址都是有效值,在重定位,合并符号表的时候就会产生冲突和歧义。
②C++为什么支持函数重载
C++会对写入符号表的同名函数进行名字修饰(name Mangling),这样函数重载的函数的名字都不一样,有效地址也不一样,这样合并符号表就不会发生冲突。
注:不同编译器名字修饰的方法不同。 图为举例。
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为即使返回值不同,修饰的函数不同解决了符号表合并时的函数名冲突,但是真正调用时只有实参无法确定使用哪个函数。
6. 引用
1)引用概念和使用
引用变量是一个别名,把该引用初始化为某个变量x,就可以使用该引用名称或变量名称x来读取或者修改x变量。此时两者是同一个东西。
使用方法:引用类型必须和引用实体是同种类型的
类型& 引用变量名(对象名) = 引用实体;
int a=10;
int& a_othername=a;
2)引用特性
a. 引用在定义时必须初始化
b. 一个变量可以有多个引用
c. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = ra;
printf("%p %p %p\n", &a, &ra, &rra);
}
3)常引用
指针和引用赋值/初始化时权限只能缩小或者平移,但不能放大
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量,需要用const类型接收
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量,需要用const类型接收
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d; //隐式类型转换
/*
int i=1;
double d=i;
//隐式类型转换
*/
4)使用场景
a. 做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
b. 做返回值
1)值返回
不管返回的值在栈,堆,静态区的任何地方,值返回都会产生临时变量。
2)引用返回
只要出了函数后该返回的值还存在,就可以通过引用返回,直接将返回值返回,省去临时变量的传递。
int& Add(int a, int b)
{
int c = a + b;//错误
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。
传值、传引用效率比较
-以引用作为参数,在传参期间,函数会直接传递实参,而传值需要多一份临时的拷贝,因此用值作为参数效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
-返回引用实际返回的是一个指向返回值的隐式指针,在内存中不会产生副本,是直接将返回值拷贝给接收变量,这样就避免产生临时变量,相比返回普通类型的执行效率更高,而且这个返回引用的函数也可以作为赋值运算符的左操作数
5)引用与指针
语法层面:引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
引用在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
7. 内联函数
在C语言里宏分为宏常量,宏函数。然而宏具有许多不安全的问题,且不易调试。c++里引入了内联函数代替宏函数,一般提倡使用const和enum替代宏常量。
1)概念
以inline修饰的函数叫做内联函数。
我们知道,一般情况下,所有函数调用时都需要建立栈帧和传递参数。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会把该函数的代码副本放置在每个调用该函数的地方,用函数体替换函数的调用。不会产生建立栈帧和传递参数的消耗,提升程序运行的效率。
2)特性
a. inline是一种以空间(目标文件的空间)换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体展开替换函数调用,缺陷:可能会使目标文件体积变大,优势:少了调用开销,提高程序运行效率。
b. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
c.inline不建议声明和定义分离,分离会导致编译时链接错误。因为inline函数会被展开,没有函数地址,不会进入符号表,这样进行链接时会发生链接错误,找不到函数定义。
// 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 中被引用
d.在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
8. auto关键字(C++11)
1)概念和使用
a.引入
随着程序越来越复杂,程序中用到的类型也越来越复杂,像:
std::map<std::string, std::string>::iterator
这样的类型太长了,特别容易写错。在C语言里可以通过typedef给类型取别名解决:
typedef std::map<std::string, std::string> Map;
int main()
{
Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
Map::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
但是typedef还是存在一些问题:
b.概念
为了解决这个问题,并且在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易。因此C++11给auto赋予了新的含义。auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int a = 10;
auto b = a;
auto c = 'a';
auto d = fun();
auto& ra=a;
auto* pa=&a;
auuto e;//error,报错
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编
译期会将auto替换为变量实际的类型。
2)特性
1. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
c. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
d. auto不能直接用来声明数组
e.为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
f.auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,和lambda表达式等进行配合使用。
9. 基于范围的for循环(C++11)
1 )范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
比
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2) 范围for的使用条件
a. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
//函数里,数组名array已经退化成了指针
b. 迭代的对象要实现++和==的操作。
10. 指针空值---nullptr(C++11)
1)NULL
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现
不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下
方式对其进行初始化:
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)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的
初衷相悖。第二次调用时f会匹配到int参数那个。
2)nullptr
c++引入了nullptr完善NULL,其实质是(void*)0
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void
*)0。
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。
2.在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。