C++入门知识总结
- 1.什么是C++
- 2. C++关键字(C++98)
- 3.命名空间
- 3.1命名空间定义
- 1.命名空间的普通定义
- 2.命名空间的嵌套定义
- 3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间
- 3.2命名空间的使用
- 1.加命名空间名称以及作用域限定符
- 2.使用using将命名空间中成员引入
- 3.使用using namespace 命名空间名称引入
- 4 C++输入&输出
- 4.1输出
- 4.2输入
- 5.缺省参数
- 5.1概念
- 5.2缺省参数的分类
- 1.全缺省参数
- 2.半缺省参数
- 6.函数重载
- 6.1函数重载概念
- 6.2 C++支持函数重载的原理--名字修饰(name Mangling)
- 7 引用
- 7.1概念
- 7.2 引用特性
- 7.3引用使用场景
- 7.4传值,传引用效率比较
- 1.引用做参数性能比较
- 2.值和引用的作为返回值类型的性能比较
- 7.5常引用
- 1.常引用之引用做参数
- 2.常引用
- 7.6引用和指针的区别
- 8.内联函数
- 8.1概念
- 8.2特性
- 9.auto关键字(C++11)
- 9.1 类型别名思考
- 9.2 auto简介
- 9.3auto的使用细则
- 9.4 auto不能推导的场景
- 10. 基于范围的for循环(C++11)
- 10.1 范围for的语法
- 10.2 范围for的使用条件
- 11. 指针空值nullptr(C++11)
1.什么是C++
C++是一种面向对象的计算机程序设计语言,作为C语言的继承,C++不仅能进行C语言的过程化程序设计,而且可进行以抽象数据类型为特点的基于对象的程序设计,还能进行基于过程的程序设计。
2. C++关键字(C++98)
C++总计63个关键字,其中C语言占32个关键字
画圈的是C语言的关键字,所以说C++兼容C;
3.命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main()
{
printf("%d\n", rand);能正确打印吗?
return 0;
}
程序运行起来会报这样的错:错误 C2365 “rand”: 重定义;以前的定义是“函数”
原因是,在C语言中rand是一个生成随机数的函数,包含在stdlib.h头文件中。头文件在预处理的时候就会完成包含(可以想象就是把头文件里的内容展开),如果不太清楚这块可以看这里浅淡代码如何从源文件转变成可执行文件。而在下面我们又重新定义了rand,结果造成了命名冲突。所以就报错了。
3.1命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后**接一对{}**即可,{}中即为命名空间的成员。
针对上面代码做出的修改
#include<stdio.h>
#include<stdlib.h>
//命名空间也可以叫做域
//申请了一个叫bit的域
namespace bit
{
int rand = 0;
}
int a=10;
int main()
{
printf("%d\n", bit::rand);//打印域里
printf("%d\n", ::a);//打印全局变量
return 0;
}
在上述代码中发现,想要访问域(命名空间)里的东西,需要::作用符;这个符号是域作用限定符,bit::rand的意思是,去bit域里找rand变量,如果找到打印,没有找到会报错,注意如果::域作用限定符前面没有域名,就会打印全局变量;
总结一下
域不影响生命周期,因此域里的变量是全局变量并不是局部变量。其次,域是一种编译的查找规则。
默认的查找规则是:先在局部找,再全局找。
现在有了域就多了一个查找方式。
1.默认查找
2.去指定域查找
1.命名空间的普通定义
//1,普通命名空间,不仅可以定义变量,还可以定义函数,和类型
namespace bit
{
int rand = 0;
int Add(int x, int y)
{
return x + y;
}
struct Node
{
int a;
//...
};
}
int main()
{
printf("%d\n", bit::rand);
printf("%d\n", bit::Add(1, 2));
struct bit::Node node;//结构体这里需要注意一下
return 0;
}
2.命名空间的嵌套定义
//2.命名空间可以嵌套
namespace N1
{
int a = 1;
namespace N2
{
int b = 3;
}
}
int main()
{
printf("%d\n", N1::a);
//N1里找N2,N2里找b,找不到就报错
printf("%d\n", N1::N2::b);
return 0;
}
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间
在写一个工程时,声明和定义一般都是分开写
//在Stack.h声明
namespace bit
{
struct STNode
{
int a;
int size;
int capacity;
};
void StackInit(struct STNode* ps);
//...
}
//在Stack.cpp定义
namespace bit
{
void StackInit(struct STNode* ps)
{
ps->a = NULL;
ps->size = ps->capacity = 0;
}
//...
}
//test.cpp调用
int main()
{
bit::STNode st;
bit::StackInit(&st);
return 0;
}
3.2命名空间的使用
命名空间的使用有三种方式:
1.加命名空间名称以及作用域限定符
namespace bit
{
int b = 10;
}
int main()
{
printf("%d\n", bit::b);
return 0;
}
2.使用using将命名空间中成员引入
namespace bit
{
int b = 10;
}
using bit::b;
int main()
{
//printf("%d\n", bit::b);
printf("%d\n", b);
return 0;
}
3.使用using namespace 命名空间名称引入
namespace bit
{
int a = 10;
int b = 20;
}
using namespace bit;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
}
上面三种方法总结起来就三种情况;
1.全部围住
想象头顶有一片果林,用bit这个围墙给围起来,如果我不告诉你,里面有什么水果,你永远都不知道。
一般做大型项目可以这样做,确保安全。
2.指定展开
指定展开也可以叫做-----常用展开,把自己经常用的展开,但是要注意,自己定义变量时的时候要避免与常用重名。
3.全部展开
把这片果林,bit围墙全部拆了,想吃就拿,虽然比较方便了,但是不太安全,建议日常练习可以这样用。
4 C++输入&输出
上面都还是C语言的输出,我们接下来看看C++如何输出输入的。
4.1输出
#include<iostream>
using namespace std;
int main()
{
//<< 流插入
std::cout << "hello C++" << std::endl;
//使用using 就可以这样写
cout << "hello C++" << endl;
return 0;
}
说明:
1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
-
cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出(也可以使用’\n’),他们都包含在包含< iostream >头文件中。
-
<< 是流插入运算符,>> 是流提取运算符。
-
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型
但是如果让打印出每个变量的类型,C++就会显得麻烦
int main()
{
//std::cout << "hello C++" << std::endl;
//cout << "hello C++" << endl;
int a = 10;
double d = 13.14;
char c = 'a';
//自动识别类型
cout <<"int :" << a << endl;
cout<<"double:" << d << " " <<"char:" << c << endl;
return 0;
}
使用C语言就会觉得好一点,C++包容C,所以觉得那种方便就用那种。
4.2输入
int main()
{
int a;
double d;
char c;
//>>流提取
//自动识别类型
cin >> a;
cin >> d >> c;
cout << a << endl;
cout << d << " " << c << endl;
return 0;
}
5.缺省参数
5.1概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void func(int x = 10,int)
{
cout << x << endl;
}
int main()
{
func();//没有传参时,使用参数的默认值
func(20);//传参时,使用指定的实参
return 0;
}
注意传参的时候不能隔开传,只能从左往右依次传
void func(int x = 10,int y=20,int z=30)
{
cout << "x = " << x << endl;
cout << "y = " << x << endl;
cout << "z = " << x << endl;
}
int main()
{
func(1);
func(1, 2);
func(1, , 3);//报错
return 0;
}
5.2缺省参数的分类
1.全缺省参数
void func(int x = 10,int y=20,int z=30)
{
cout << "x = " << x << endl;
cout << "y = " << x << endl;
cout << "z = " << x << endl;
}
2.半缺省参数
void func(int x, int y = 20, int z = 30)
{
cout << "x = " << x << endl;
cout << "y = " << x << endl;
cout << "z = " << x << endl;
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现
//在a.h声明
void func(int x = 10);
//在a.c定义
void func(int x = 20)
{
cout << x << endl;
}
//注意:如果声明和定义都同时赋缺省值,但是这两个位置的值不同,编译器就无法确定该用那个缺省值
所以缺省值一般只在声明中给
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
6.函数重载
6.1函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
int Add(int x, int y)
{
return x + y;
}
//1.参数类型不同
double Add(double x, double y)
{
return x + y;
}
int main()
{
Add(1, 2);
Add(1.0, 2.0);
}
int Add(int x, int y)
{
return x + y;
}
//2.参数个数不同
int Add(int x)
{
}
int main()
{
Add(1, 2);
Add(1);
}
注意看下面这段代码
int Add(int x, int y)
{
return x + y;
}
//这里是顺序不同吗
int Add(int y, int x)
{
return y + x;
}
int main()
{
//当程序执行的时候,会报错
Add(1, 2);
}
// error C2084: 函数“int Add(int,int)”已有主体
// message : 参见“Add”的前一个定义
// error C2065: “Add”: 未声明的标识符
注意
顺序不同是指形参类型顺序不同
//这里才是正确的
//3.参数顺序不同
int func(int x, char y)
{
cout<< x << " " << y << endl;
}
int func(char x, int y)
{
cout<< x << " " << y << endl;
}
学到这里我们有个想法,上面我们学了缺省参数,那么缺省参数和函数重载能放在一块使用吗?
由上图说明,缺省函数和函数重载是不能在一块使用的,可能会导致二义性
6.2 C++支持函数重载的原理–名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
-
实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
-
所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。具体想了解这一块可以看浅淡代码如何从源文件转变成可执行文件
-
那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则
-
由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。
-
通过下面我们可以看出C的函数修饰后名字不变。而C++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
采用C语言编译器编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
采用C++编译器编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
函数名修饰规则:参数不同,修饰出来的名字不同
既然因为这样,那我们考虑这样一个问题,返回值不同是否构成函数重载呢?
//根据函数名修饰规则
//_z4ifunii
int func(int a, int b)
{
cout << a << " " << b << endl;
}
//_z4dfuncii
double func(int a, int b)
{
cout << a << " " << b << endl;
}
从表面上看,返回值不同,似乎也可以函数重载,因为函数名是不一样的。
但,这却不是对的
返回值不同,不构成重载的原因,并不是因为函数名修饰规则
真正原因是调用时的二义性,因为我们调用函数时不能指定返回值类型
7 引用
7.1概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体;
int main()
{
int a = 10;
int& ra = a;
int& x = a;
int& y = x;
ra++;
x++;
y++;
cout << a << endl;//打印13
return 0;
}
注意:引用类型必须和引用实体是同种类型的。
7.2 引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
int main()
{
int a = 0;
int& ra=a;
int& rra;//编译时会报错
//引用定义必须初始化
int b=20;
ra=b;//这里是改引用还是赋值呢?
//其实是赋值,a的值变成了20
return 0;
}
看下面一段代码
int main()
{
int a = 0;
int& ra=a;
int* p = &a;
ra++;//1
(*p)++;//2
return 0;
}
感觉指针和引用好像是同一个东西,但是在C++ 引用不能完全替代指针
比如实现一个链表,插入和删除需要改变指针的指向,但是使用引用不能改变结点指向。
引用和指针的关系
7.3引用使用场景
1.做参数
//输出型参数
int Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
2.做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
传值返回
2.引用返回
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;
//n在静态区,这里打印ret是1没问题
return 0;
}
int& Count()
{
int n = 0;
n++;
//...
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;//这里也是打印1,但是真对了吗
return 0;
}
第二次打印ret打印就是随机值,这是因为,Count这块空间被其他内容覆盖了。(cout也是一个函数,这块空间被cout覆盖了。后面我们再说cout函数。)
总结一下
出了函数作用域,返回变量不存在,不能用引用,因为引用返回结果是未定义的,如果返回变量还存在,可以用引用。
7.4传值,传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
1.引用做参数性能比较
#include <time.h>
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;
}
总结
引用做参数
1.减少拷贝,提高效率
2.输出型参数,函数中修改形参,实参也会改变。
2.值和引用的作为返回值类型的性能比较
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
再看下面一段代码
//seqlist.c
size_t SeqlistSize(SL* psl)
{
assert(psl);
return psl->count;
}
SLDataType& SeqlistAt(SL* psl, size_t pos)
{
assert(psl);
assert(pos < psl->count);
return psl->a[pos];
}
#include"seqlist.h"
int main()
{
SL st;
SeqlistInit(&st);
SeqlistPushBack(&st, 1);
SeqlistPushBack(&st, 2);
SeqlistPushBack(&st, 3);
SeqlistPushBack(&st, 4);
//一起我们还需要写修改函数,现在就可以把修改函数淘汰了
//将偶数乘2
for (int i = 0; i < SeqlistSize(&st); ++i)
{
if (SeqlistAt(&st, i) % 2 == 0)
{
SeqlistAt(&st, i) *= 2;
}
}
//打印
for (int i = 0; i < SeqlistSize(&st); ++i)
{
cout << SeqlistAt(&st, i) << endl;
}
SeqlistDestory(&st);
}
总结
传引用返回
1.减少拷贝,提高效率
2.修改返回值
7.5常引用
1.常引用之引用做参数
int main()
{
int a = 10;//a可读可写
//我引用你,权限平移,可以
int& ra = a;//ra可读可写
//指针和引用赋值时,权限可以变小,平移,不能变大
//我引用你,权限变小,可以
const int& rra = a;//rra可读
const int b = 20;//b可读
//我引用你,权限变大,不可以
int& rb = b;//编译会报错 rb可读可写
}
看下面一段代码
//为了减少拷贝,但是第二次,第三次调用都会报错
void func(int& x);
{
}
//使用这个下面传参就不会报错了,三次调用都没问题
void func(int x)
{
}
int main()
{
int a = 10;//a可读可写
//我引用你,权限平移
int& ra = a;//ra可读可写
//指针和引用赋值时,权限可以变小,平移,不能变大
//我引用你,权限变小
const int& rra = a;//rra可读
const int b = 20;//b可读
//我引用你,权限变大
int& rb = b;//编译会报错 rb可读可写
func(a);
func(b);//报错 权限变大
func(rra);//报错 权限变大
//使用第二个func函数,就相当于下面,把b赋值给a,改变a并不会改变b
a=b;
}
如果把引用去掉,变成void func(int x);就不会报错,因为形参时实参的临时拷贝;
如果就是用引用做参数的话,就在前面加个const,在C++库里面实现的函数,形参都加了const修饰。
void func(const int& x);
{
}
int main()
{
int a = 10;//a可读可写
//我引用你,权限平移
int& ra = a;//ra可读可写
//指针和引用赋值时,权限可以变小,平移,不能变大
//我引用你,权限变小
const int& rra = a;//rra可读
const int b = 20;//b可读
//我引用你,权限变大
int& rb = b;//编译会报错 rb可读可写
//都可以
func(a);
func(b);
func(rra);
}
如果非要修改这个形参,就不要使用const修饰;
2.常引用
int main()
{
//加const 引用可以赋值常量
const int& b = 10;
//因此前面的函数,我们还可以传常量
//func(10);
double d = 12.13;
int t = d;//不报错
int& a = d;//报错
return 0;
}
什么原因导致报错呢?
可能头脑马上有一个想法,引用两边类型不一样所以导致报错。
但真的是这样吗?
注意:临时变量具有常性,不能修改
int main()
{
//加const 引用可以赋值常量
const int& b = 10;
//因此前面的函数,我们还可以传常量
//func(10);
double d = 12.13;
int t = d;//不报错
//int& a = d;//报错
//加上const就可以了
const int& a = d;//正确
return 0;
}
还得注意一种情况
int func(int x = 10)
{
return x;
}
int main()
{
//引用接收,传值返回也会报错
int& ret = func();
//上面我们说过,传值返回,返回的是临时变量,因此报错
//加上const就可以了
const int& ret = func();
return 0;
}
7.6引用和指针的区别
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看一下引用和指针的汇编代码对比
会发现引用和指针在底层实现是一样的
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
8.内联函数
8.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
我们知道函数调用的是需要开辟栈帧。而调用栈帧是需要消耗程序运行效率的。
想要看自己的代码效果,需要在调试的时候转到反汇编。
就比如说快速排序,一趟排序需要调用好几次交换函数,这个交换函数虽然很小,但是确频繁调用,就会有栈帧开销。
C语言的解决方法-----------------宏函数 不清楚的看这里(超详细的预处理详解)
但是宏函数有一些缺点1.不能调试,2没有对类型安全检查,3容易写错等等。
#define Add(x,y) ((x)+(y))
int main()
{
//写一个Add函数,用宏的方式
//宏直接替换
cout << Add(1, 2) << endl;
return 0;
}
写宏容易犯的错误
//记住下面对应错误情形
#define Add(x,y) ((x)+(y)
//#define Add(x,y) ((x)+(y));
//不能加分号
if (Add(1, 2))
{
//...
}
//加外层括号
Add(1, 2) * 3;//会和预想值不一样
//加内括号
int main()
{
int a = 1, b = 2;
Add(a | b, a & b);//不加内括号,可能因为运算符优先级不一样,造成结果错误
}
C++解决方法-------------------内联函数
inline克服了宏函数的缺点
在debug模式下,需要对编译器进行设置,否则不会展开
对比两种图片发现,使用inline,C++确实没有调用Add函数开辟栈帧,直接把内容展开了。
建议:频繁调用的小函数使用内联
8.2特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
这个空间实际上指的是编译出来的执行程序/静态库/动态库大小。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
有人为了避免栈帧开销,而给每个函数前都加上inline,但是这只能算是一个建议,到底用不用内联取决于每个编译器的内部实现。
为什么函数长了不展开?
原因是因为,代码膨胀。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
下面我们从程序编译链接来分析,不太懂这里的可以看浅淡代码如何从源文件转变成可执行文件
声明和定义分离
普通函数
内联函数
这里可能会有这样一个疑问,为什么预处理阶段test.cpp完成头文件的替换,明明有inline了,而且Add函数内容还小,为什么不展开,还会报链接错误?
首先,因为这里只是声明没有定义,所以没有内容可展开的。
其次,也可以这样想,只要有inline,Add就不会进入符号表,因此有链接错误。注意每个.cpp文件时独立进行的,在调用这个函数的时候编译器根据情况,在决定对这个函数展不展开。
所以写项目,使用内联函数,不建议声明的定义分离,直接去头文件中定义
总结一下
宏的优缺点
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用const enum
- 短小函数定义换用内联函数
9.auto关键字(C++11)
9.1 类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
#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();
return 0;
}
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。有的人可能已经想到:可以通过typedef给类型取别名,比如:
#include <string>
#include <map>
typedef std::map<std::string, std::string>::iterator Map;
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },{"pear","梨"} };
//std::map<std::string, std::string>::iterator it = m.begin();
Map it = m.begin();
return 0;
}
使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义
9.2 auto简介
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto根据右边自动识别左边类型
int Add(int x,int y)
{
return x + y;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto k=10;
auto ret = Add(1,2);
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注意
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
9.3auto的使用细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int a = 10;
//指针
auto* p= &a;
auto pp = &a;
//引用
auto& ra = a;
return 0;
}
typeid().name()作用:拿到一个变量类型变成字符串
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
auto a = 1, b = 2;
auto c = 3, d = 13.14;//报错
return 0;
}
9.4 auto不能推导的场景
1.auto不能推导的场景
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
//就不知道TestAuto栈帧开多大
void TestAuto(auto a)
{}
2. auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
-
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
-
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
10. 基于范围的for循环(C++11)
10.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 };
//依次取array中的数据赋值给饿,自动判断结束,自动迭代
//想要改变数组中的内容,使用&引用,不然赋值给e,改变e并不能改变数组
for (auto& e : array)
{
e *= 2;
}
for (auto e : array)
{
cout << e << endl;
}
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
10.2 范围for的使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
//因为数组传参传的是数组名,数组名是数组的首地址
void TestFor(int arr[])
{
for (auto& e : array)
{
cout << e << " " >> endl;
}
}
2. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,后面再说)
11. 指针空值nullptr(C++11)
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int main()
{
int* p = NULL;
int* pp = 0;
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的都会遇到一些麻烦,比如下面
本来下通过func(NULL)来调用f(int*)函数的,结果确与我们的想法违背。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
因此C++11中,一般定义空指针应该像下面这样
int main()
{
int* p = nullptr;
return 0;
}
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
自此C++入门知识总结完成。有什么不太懂的,欢迎点赞,评论,加收藏。