目录
正文:
1.C++关键词
C++G共63个关键字,C语言共32个关键字,此处不做详细介绍。
2.C++命名空间
在写C++代码前,我们总是会默认编写一行代码为using namespace std;
C++为了防止命名冲突,将标准库的东西都放置于std的命名空间内。
这行代码的含义是:展开C++标准库。
(1):
#include<iostream>
int main()
{
printf("%d",rand);
return 0;
}
编译器会报错:
因为:当运行时会默认首先在局部域寻找变量rand,寻找失败时会到全局域中寻找,全局域中有两个rand,一个是标准库中的rand函数,一个是定义的全局变量rand=0;此时编译器就会报错。
(2):
#include<iostream>
namespace bit
{
int rand = 0;
}
int main()
{
printf("%d\n", rand);
//打印的是标准库中rand函数的地址
printf("%d\n", bit::rand);
//打印的是bit命名空间中的rand
return 0;
}
设置bit命名空间,然后通过指定打印的rand是bit命名空间的rand,成功打印0;
(3):
namespace bit
{
int rand = 0;
}
using namespace bit;
//展开bit命名空间
int main()
{
printf("%d\n", rand);
return 0;
}
using namespace bit;相当于将命名空间bit中的内容展开放置于全局域中,即全局域中又出现了2个rand,此时编译器仍会报错。
(4):
C++将标准库的内容放置标准库std中是为了避免命名冲突,而using namespace std;将标注库的东西全部暴露在全局域中,其实并不方便。
在日常练习中我们可以直接展开std命名空间:
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
但是在项目中还需尽量避免using namespace std;的使用,可以通过指定命名空间+展开常用命名空间的方法:
#include<iostream>
#include<vector>
using std::cout;
using std::endl;
//将std中的cout与endl展开放置全局域中
int main()
{
std::vector<int>v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
cout << "hello" << endl;
cout << "world" << endl;
return 0;
}
(5):支持自定义命名空间
namespace N1
{
int a;
int Add(int left, int right)
{
return left + right;
}
//命名空间内可以定义变量、函数、结构体类型等等
}
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
//命名空间可以嵌套
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left + right;
}
}
}
同时,同一个工程中允许存在多个形同名称的命名空间,比如我们在test.h中也定义了namespace N1,编译器会将其合并在同一个命名空间中:
//test.h
//namespace N1
{
int Mul(int left,int right)
{
return left+right;
}
}
3.C++输入和输出
C++改进了C语言在输出时需要指定类型这一特点,可以自动识别类型:
using namespace std;
int main()
{
int i;
double d;
// >>流提取运算符
cin >> i >> d;
// <<流插入运算符
cout << i << endl;
//endl表示换行操作,等同于
//cout<<i<<'\n';
cout << d << endl;
return 0;
}
PS: ①:
② : IO流控制精度,输出宽度等等操作都较为麻烦,在编写代码中如果需要,可以进行C语言的穿插,二者兼容,如:
#include<iostream>
using namespace std;
int main()
{
float d=0;
cin>>d;
cout<<d<<endl;
printf("%.2lf",d);
return 0;
}
③ :使用cin标准输入 和cout标准输出时,必须包含<iostream>头文件以及std标准命名空间。
早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件分开,也为了正确使用命名空间,规定C++头文件不带.h。部分旧编译器(vc 6.0)还支持<iostream.h>的使用,但大部分更新的编译器已不支持,故而推荐使用<iostream>+std的方式。
4.缺省参数
4.1 缺省参数的定义
缺省参数是声明或定义函数时为函数的参数制定一个默认值。在调用该函数时,如果没有指定实参,则采用该默认值。否则就使用指定的实参。
#include<iostream>
using namespace std;
void Fun(int a=0)
{
cout << a << endl;
}
int main()
{
Fun(1);
Fun(2);
Fun(3);
Fun();
return 0;
}
输出结果为:
4.2 缺省参数的分类
(1)全缺省:多个参数全部缺省
#include<iostream>
using namespace std;
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout << "a=" << a << '\t';
cout << "b=" << b << '\t';
cout << "c=" << c << '\t' << endl;
}
int main()
{
TestFunc(1, 2, 3);
TestFunc(1, 2);
TestFunc(1);
TestFunc();
return 0;
}
运行结果如下:
(2)半缺省:
#include<iostream>
using namespace std;
void TestFunc(int a , int b = 10, int c = 20)
{
cout << "a=" << a << '\t';
cout << "b=" << b << '\t';
cout << "c=" << c << '\t' << endl;
}
int main()
{
TestFunc(1, 2, 3);
TestFunc(1, 2);
TestFunc(1);
return 0;
}
运行结果如下:
PS:①:无论是全缺省还是半缺省,都是从左往右传参;
void Fun1(int a=1,int b=2,int c=3)
{
//...
}
int main()
{
Fun1(20,30)
return 0;
}
即在上文代码中,20必须传参给a,30必须传参给b;
同时,下文代码的格式也是错误的:
void Fun(int a=1,int b=2,int c=3)
{
//...
}
int main()
{
Fun( ,20, );
return 0;
}
②:半缺省必须从右往左连续缺省,不能间隔;
即下文的缺省方式都是错误的:
void Func1(int a=1,int b,int c=2);
void Func2(int a,int b=3,int c);
③:缺省参数不能在函数定义和声明中同时出现:
如果声明和定义同时出现,而两个位置的缺省参数不同,那么编译器就无法确认到底使用哪个缺省值。
在声明中确定缺省参数即可,在定义中确定缺省参数无效。
4.3 缺省参数的应用
#include<iostream>
using namespace std;
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps, int capacity = 4)
{
ps->a = (int*)malloc(sizeof(int) * capacity);
ps->top = 0;
ps->capacity = capacity;
}
int main()
{
struct Stack st1;
//已知需要插入100个数据的空间,则实参定为100
StackInit(&st1, 100);
struct Stack st2;
//当不确定容量时,不传递容量实参,缺省参数为4,提前开好空间,初始插入数据避免扩容
StackInit(&st2);
return 0;
}
5.函数重载
5.1 函数重载的定义
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(形参个数、类型、顺序)必须不同,常用来处理实现功能类似,数据类型不同的问题。
#include<iostream>
using namespace std;
//形参类型不同
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left + right;
}
//形参顺序不同
void func(int i, char ch)
{
cout << "void func(int i,char ch)" << endl;
}
void func(char ch, int i)
{
cout << "void func(char ch,int i)" << endl;
}
//形参个数不同
int Mul(int x, int y, int z)
{
return x * y * z;
}
int Mul(int x, int y)
{
return x * y;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
func(1, 'a');
func('a', 1);
cout << Mul(2, 3, 4) << endl;
cout << Mul(2, 3) << endl;
return 0;
}
即函数重载时,要让编译器根据参数可以区分到底将形参传递给哪个函数即可。
PS:返回值不同不构成重载:
short Add(short x,short y)
{
return x+y;
}
int Add(short x,short y)
{
return x+y;
}
上述代码不构成重载。
5.2 函数重载的意义
C++语法中不需要区别输出数据类型,cout<<可以直接进行类型的识别,其本质就是函数重载:
函数重载的意义,在于调用函数时非常方便,就像在调用一个函数一样;
5.3 名字修饰
C++支持函数重载而C语言不支持,这与C++的名字修饰有关。
此处不做详细解释。
6.引用
6.1 引用的定义
引用不是新定义一个变量,而是给已存在的变量取一个别名,编译器不会为了引用变量开辟内存空间,它和它引用的变量共享一块内存空间。
类型&引用变量名(对象名)=引用实体;
6.2 引用特性
(1)引用在定义时必须初始化;
int a=1;
int& b;
上文代码错误,必须阐明定义的是哪一个变量的引用;
(2)一个变量可以有多个引用,也可以给引用定义引用:
int a = 1;
int& b = a;
int& c = a;
int& d = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
输出结果为:
虽然命名不同,但是变量abcd均代表同一个地址存储的变量;
(3)引用一旦引用一个实体,再不能引用其他实体;
int a = 1;
int& b = a;
int x = 10;
b = x;
cout << &a << endl;
cout << &b << endl;
cout << &x << endl;
输出结果为:
a与b的地址相同,x的地址与ab不同;
b已经是a的引用,则不能再将b变为x的引用,b=x的含义是将x赋值给b而非b是x的引用;
PS:
//1.当&符号处在在类型与变量之间时,表示引用
int a=0;
int& b=a;
//2.当&符号单独与变量存在时,表示取地址
cout<<&b<<endl;
6.3 使用场景
(1)作参数——作为输出型参数
① 应用于实现交换的函数:
#include<iostream>
using namespace std;
void Swap(int& r1,int& r2)
{
int& tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int a = 0, b = 2;
Swap(a, b);
return 0;
}
② 应用于单链表:
#include<iostream>
using namespace std;
typedef struct SListNode
{
int data;
struct SListNode* next;
}SLTNode;
void SListPushBack1(SLTNode** pphead, int x)
{
//...
}
void SListPushBack2(SLTNode*& phead, int x)
{
if (phead = NULL)
{
phead = (SLTNode*)malloc(sizeof(SLTNode));
}
//...
}
int main()
{
SLTNode* list = NULL;
SListPushBack1(&list, 1);
SListPushBack1(&list, 2);
SListPushBack1(&list, 3);
SListPushBack2(list, 1);
SListPushBack2(list, 2);
SListPushBack2(list, 3);
return 0;
}
PS:输入型参数:传入函数供使用的参数;
输出型参数:函数内改变,在函数外拿到已经改变后的参数;
(2)作返回值
① 传值返回:
② 传引用返回:
using namespace std;
int& Count()
{
int n = 0;
n++;
//...
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;
cout << ret << endl;
return 0;
}
运行结果如下:
传值返回方式返回的是n的临时拷贝,而传值返回方式返回的是n的别名,当Count函数调用完毕,变量n原先所在的空间已经归还,而此时却将n的别名作为返回值给ret,即将原存储变量n的那块空间的值给ret,即非法占用了已归还操作系统的空间。
当函数调用完毕,n及其引用值所在的Count栈帧内的空间都会被销毁,即n及其引用值地址丢失,故而ret的结果是未定义的,栈帧销毁时,系统清理栈帧会将ret置为随机值。
第一次输出结果仍为1是因为原栈帧还未被随机值覆盖,第二次输出结果为随机值是因为第一次调用的printf函数占用了Count函数的栈帧,将这块区域被随机值覆盖,当main函数再次call printf时就会打印随机值。
故而,其实上述代码使用引用返回本质是错误的,发生了越界行为,结果没有保障。
PS:(1)如果出了函数的作用域,返回对象销毁(如函数中的局部对象),则一定不能用引用返回,一定要用传值返回。
(2)当函数内创建变量用static修饰时,变量出函数作用域并未被销毁,就可以使用引用返回。
即下文代码是正确的:
using namespace std;
int& Count()
{
static int n = 0;
n++;
//...
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;
cout << ret << endl;
return 0;
}
6.4 传值、传引用效率(性能)比较
#Include<iostream>
#include <time.h>
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;
}
输出结果如下:
可见,当大对象传参时,传引用效率更高;
PS:① 关于const修饰的权限平移、放大与缩小:
试运行以下程序:
int a = 10;
int& b = a;
const int c = 20;
int& d = c;
报错如下:
是因为
//权限平移:
int a = 10;
int& b = a;
//权限缩小:
int c = 20;
const int& d = c;
//权限由可读可写变为仅可读,权限缩小;
const int e = 30; //const修饰变量e表示:变量e具有常性,不能修改
//权限放大:
//int& f = e;
//此处将f定义为e的别名,意味着代表同一块空间,
//但定义f的类型却是int,将一块不能改变内容的空间变为可改变
权限可以平移和缩小,但不可以放大。
同时注意,权限的缩小和放大只针对于存在引用时的状况,一般变量的传参不需要考虑权限的缩小放大。在引用存在的函数传参时,建议用const修饰形参避免权限扩大出现错误。
② 隐式类型转换:
int i = 10;
double j = i; //隐式类型转换
//错误写法:
double& k = i;
//类型转换时,会产生一个double类型的临时变量
//i将值赋给临时变量,再由临时变量赋给k
//临时变量具有常性,故而不可直接将可读可写的变量i赋值给只可读的临时变量
//此行为导致权限放大,故而错误;
const double& k = i;
//const修饰后,即可保证权限的缩小与平移,编写正确;
同时请注意,强转等类型转换都不会改变原变量的类型,都会产生一个临时变量;
故而建议使用引用时建议使用const,会减少可能出现的错误;
③ 权限的缩小平移放大等只针对指针和引用,对于函数调用等普通变量之间赋值情况与之无关:
运行以下代码:
void func1(int n)
{
cout << n << endl;
}
int main()
{
int a = 10;
const int b = 20;
func1(a);
func1(b);
func1(30);
return 0;
}
输出结果如下:
④ 当函数为引用传参时,建议用const修饰:
void func2(int& n)
{
cout << n << endl;
}
int main()
{
int a = 10;
const int b = 20;
func2(a);
func2(b); //错误
func2(30); //错误
return 0;
}
报错如下:
在传参时,如果形参为引用,那么当const修饰的变量或常量作为实参进行传参时,就会出现权限的放大而导致错误,故而建议当函数形参为引用时,使用const修饰形参,避免出现权限放大的情况:
void func2(const int& n)
{
cout << n << endl;
}
int main()
{
int a = 10;
const int b = 20;
func2(a);
func2(b);
func2(30);
return 0;
}
上文代码正确;
6.5 指针和引用的区别
(1)语法角度而言:
① 引用没有开空间,指针开了4或8个字节;
② 引用在定义时必须初始化,指针没有要求;
③ 引用在初始化时引用一个实体后就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
④ 没有NULL引用,但有NULL指针;
⑤ 在sizeof中含义不同,引用结果为引用类型的大小,指针始终是地址空间所占字节个数;
⑥ 有多级指针,但没有多级引用;
⑦ 访问实体方式不同,指针需要显式解引用,引用依赖于编译器处理;
⑧ 引用比指针使用起来相对更安全;
(2)从汇编码看:
底层角度而言,引用底层是用指针实现的;
总而言之,指针更强大更复杂更危险,引用更局限更安全更简单。
7.内联函数
7.1 概念
(1)以inline修饰的函数叫内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率;
比如像Swap这种短小的函数需要频繁的、如上万次调用,C语言会利用宏函数,C++则利用inline函数。
(2)对于C的宏函数,宏的缺点有:a.可读性差,b.没有类型安全的检查,c.不方便调试等等。'
优点有:a.代码可维护性增强, b.宏函数提高效率,减少栈帧建立
(3)查看方式:
1.在release模式下,查看编译器是否生成call 函数名;
2.在debug模式下,需要对编译器进行设置,否则不会展开;
(4) 尽量使用const,enum,inline替代#define;
7.2 特点
1.inline是一种以空间换时间的做法,省去调用函数额外开销,故而代码较长或有循环、递归的函数不宜使用作为内联函数;
2.inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数a.内部实现代码指令长度较长(据编译器不同而定)b.体内有循环或递归等,编译器优化时会忽略掉内联;
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器都不支持内敛递归函数,一个75行的函数也不大可能在调用点内联展开。
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址则链接找不到;
8.auto关键字(C++11)
8.1 auto简介
auto用于自动推导变量:
8.2 auto使用场景
(1)与范围for结合使用(可用于数组遍历):
using namespace std;
int main()
{
int a[] = { 1,2,3,4,5,6 };
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
cout << a[i] << " ";
}
cout << endl;
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
a[i]++;
cout << a[i] << " ";
}
cout << endl;
//范围for
//自动依次取a的数据赋值给e
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
for (auto& e : a)
{
e++;
cout << e << " ";
}
cout << endl;
return 0;
}
(2)与指针结合使用:
using namespace std;
int main()
{
int x = 10;
auto a = &x;
//a类型为int*
auto* b = &x;
//b类型为int*,强调一定要传指针
auto& c = x;
//c类型为int,强调c是一个引用
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
8.3 auto不能使用场景
(1)auto不能作为函数的参数
void TestAuto(auto a)
{}
(2)auto不能直接用来声明数组
void TestAuto()
{
int a[]={1,2,3};
auto b[5]={4,5,6};
}
9.指针空值nullptr
NULL实际上是一个宏,在传统的C头文件中规定:#define NULL 0;
在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0;
PS:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的;
2.在C++11中,sizeof(nullptr)与sizeof(void*)0所占字节数相等;
3.为了提高代码的健壮性,在后续表示指针空值时建议使用nullptr;