C++入门
C++关键字
C语言关健字32个,C++关键字63个关键字
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 | | | |
C++是在C的基础上发展起来的,C++是兼容C的大多数的语法
命名空间
C++是怎么输出hello world的呢?和C语言有什么区别呢?
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
我们创建一个C++文件后,写出上面代码就会在屏幕上打印hello world,那么上面代码中的using namespace std;是什么意思呢?这就是我们要讲的命名空间。
C++增加命名空间是为了解决C语言的不足,在一段C语言代码中,我们编译下面的代码会报错:
#include<stdio.h>
#include<stdlib.h>
int rand =10;
int max=10;
int main()
{
printf("%d\n",rand);
return 0;
}
那么为什么会报错呢?是因为我们包含了stdlib.h这个头文件
这是一个命名重定义的问题,在我们编写C语言代码时,我们定义变量名时,可能与库函数中的命名冲突了,实际大型项目开发,还存在同事之间定义的变量、函数、类型命名冲突等等
所以为了弥补C语言的不足,C++提出了命名空间来解决命名冲突的问题
那么命名空间是怎么定义的呢?
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名 空间的成员。注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
#include<stdio.h>
#include<stdlib.h>
namespace Z
{
int rand = 10;
}
int main()
{
printf("%d\n",Z::rand);
return 0;
}
::是域作用限定符,通过这个我们可以找到命名空间中的定义的变量或者其他东西,这样我们就可以定义rand,并可以成功的打印它
我们也可以在里面定义函数:
#include<stdio.h>
#include<stdlib.h>
namespace Z
{
int Add(int left,int right)
{
return left+right;
}
}
int main()
{
Add(1,2);
Z::Add(1,2);
return 0;
}
这里需要注意的是直接调用他找不到,因为他只会在全局里面找,不会去域里面,所以我们需要利用域作用限定符去调用Add函数
命名空间还可以嵌套定义:
namespace Z
{
int rand = 10;
int Add(int left,int right)
{
return left+right;
}
namespace S
{
int Sub(int left,int right)
{
return left-right;
}
}
}
int main()
{
Add(1,2);
Z::Add(1,2);
Z::S::Sub(1,2);
return 0;
}
我们在调用嵌套的命名空间的函数时,比如我们现在调用Sub函数,我们需要用Z::S::Sub(1,2);去调用该函数
在一项工程当中不同文件的命名空间的定义名字可以相同,并且该命名空间中的定义编译器会合并在一起,比如:
在Add.cpp这个文件中我们定义了一个命名空间
namespace Z
{
int rand = 10;
}
在Add.h中我们定义了命名空间
namespace Z
{
int Add(int left,int right)
{
return left+right;
}
}
编译器会将这两个文件中定义的空间进行合并:
namespace Z
{
int rand = 10;
int Add(int left,int right)
{
return left+right;
}
}
那么我们如何使用命名空间的东西呢?
有三种方式:
namespace Z
{
int rand = 10;
int Add(int left,int right)
{
return left+right;
}
}
using namespace Z;
int main()
{
Add(1,2);
return 0;
}
优点是用起来方便。缺点是把自己的定义暴露出去了,导致命名污染
using namespace std;
std是包含C++标准库的命名空间,这里其实就将C++标准库展开了,这就解释了我们开头打印hello world时,前面为什么有一个using namespace std;这里就很好的解释了这个代码的意思
namespace Z
{
int rand = 10;
int Add(int left,int right)
{
return left+right;
}
}
int main()
{
Z::Add(1,2);
return 0;
}
优点:不存在命名污染。缺点:用起来麻烦,每个都得去指定命名空间
namespace Z
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
}
int main()
{
using Z::Add;
return 0;
}
不会造成大面积的污染,也可以解决每个都指定命名空间的问题,这是一个将1和2折中的解决方案
C++输入&输出
这又回到了我们开始提到的C++是怎么输出hello world到屏幕上的,我们来看下C++是如何来实现的:
#include<iostream>
using std::cout;
using std::endl;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
按照上面我们所讲的,std是一个命名空间,命名空间中有我们要使用的cout和endl,所以我们这里只将常用的展开,但是在日常学习中,我们并不需要像项目中那么规范,我们可以直接将std命名空间展开。
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
我们了解了输出,那么C++是如何进行输入的呢?我们使用cin标准输入,看下面代码:
#include<iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int* n = (int*)malloc(sizeof(int)*n);
for(int i=0;i<n;i++)
{
cin>>a[i];
}
for(int i=0;i<n;i++)
{
cout << a[i]<<" ";
}
cout<<endl;
cout<<"\n";
}
注意:使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空 间。早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。
我们在输出时可以连续输出,cout<<endl;实际上就等价于cout<<"\n"。
另外C语言输入输出时需要指定类型的,C++不用指定类型可以自动识别类型:
double* n = (double*)malloc(sizeof(double)*n);
for(int i=0;i<n;i++)
{
cin>>a[i];
}
int main()
{
int i=2;
double d = 1.111;
int* pi = &i;
cout<<i<<endl
cout<<d<<endl
cout<<pi<<endl;
return 0;
}
在写C语言printf和scanf函数时,前面我们需要指定输入输出的格式,而C++则是自动识别类型。
而在下面的场景中,用printf会更好一点
struct Student
{
char name[10];
int age;
};
int main()
{
struct Student s = { "张三", 18 };
cout << "名字:" << s.name << " " << "年龄:" << s.age << endl;
printf("名字:%s 年龄:%d\n", s.name, s.age);
return 0;
}
缺省参数
缺省参数概念
所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就不需要传参。可以传参数,也可以不传,如果不传,函数参数用缺省的
我们看下面代码:
#include<iostream>
using namespace std;
void TestFunc(int a = 0)
{
cout<<a<<endl;
}
int main()
{
TestFunc();
TestFunc(10);
return 0;
}
我们可以看到没有传参的使用了缺省值,而传参的就忽略缺省值了,只考虑传过来的参数
那么缺省参数有什么用呢?
我们在数据结构中,设计栈这个数据结构时:
void StackInit(struct Stack* ps, int defaultCP)
struct Stack st1;
StackInit(&st1, 100);
如果我们假设明确知道一个栈st1最少使用100个空间,我们给栈的容量初始化为100,所以这时我们在栈初始化这个接口引入一个参数defaultCP,我们想要弄100个容量,我们直接传参100就可以了。
但是当我们不能明确知道栈st2的需要使用的空间,如果上面不设置缺省参数,那么我们必须要传一个值进去,那我们传的值大了的话,会造成空间浪费,传的小了的话会不够用,那么此时这个缺省参数就派上用场了,我们这样定义:
void StackInit(struct Stack* ps, int defaultCP = 4)
struct Stack st2;
StackInit(&st2);
不传值时默认它为4,不够了再进行扩容就可以了
缺省参数分类
全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
全缺省参数顾名思义就是全部都有缺省参数,我们在传参时不能间隔着传,比如:
TestFunc(100, ,50);
编译器是不允许这样进行传参的
半缺省参数
void TestFunc(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
TestFunc(1);
TestFunc(1,2);
TestFunc(1,2,3);
return 0;
}
注意:
比如这上面的a参数必须要传参
比如:
void TestFunc(int a = 30, int b = 10, int c)
这样就是错误的,没有按照从右往左的顺序
也不能这样:
void TestFunc(int a = 30, int b, int c = 10)
void TestFunc(int a = 10);
void TestFunc(int a = 20)
{}
如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
void TestFunc(int a = x);
C语言是不支持缺省参数的。
函数重载
函数重载概念
重载函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者类型顺序)必须不同,也就是说用同一个函数完成不同的功能。
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10,20);
Add(10.0,20.0);
Add(10L,20L);
return 0;
}
函数重载面试:为什么C++支持函数重载,而C语言不支持?
C编译器,直接用函数名关联,当函数名相同时,它无法区分
C++如何支持重载呢?
函数名修饰规则:不能直接用函数名对函数进行修饰,代入参数特点修饰,函数名相同,只要参数不同,修饰出来的名字就不同,就可以区分两个函数了,就支持重载了,把定义在文件中的函数调用,地址找到(符号表里找),链接在一起,所有.o文件进行合并,生成一个执行文件。
函数重载:
要求参数不同,因为参数不同修饰出来的名字就不同
编译器能不能实现函数名相同参数相同返回值不同,就能构成重载?
不行
int func();
double func();
如果把返回值带进修饰规则,那么编译器层面是可以区分的。
但是语法调用层面,无法区分,带有严重的歧义!
func();调用时,到底是调用哪个呢?这是不知道的,所以不能
名字修饰
extern"C"
C++实现编写成动态库或者静态库,写一个C++的程序去调用这个库是没问题的,但是我们写一个C程序就不行,程序是用因为链接时会有问题,比如静态库有一个函数void* tcmalloc(size_t n) (谷歌提供的更高效替代malloc的库)
C程序在链接时,直接用函数名tcmalloc去找函数的地址,因为C++有名字修饰,而生成的符号表是:0x662521:_Z8tcmallocui,而C++是用_Z8tcmallocui去找的,C++可以找到,因为该动态库是C++写的,而C语言程序就找不到
那么有什么方式能让C程序和C++的程序都能用这个C++的库呢?
C++就出现了extern"C",在声明tcmalloc这个函数时在前面加extern"C" void* tcmalloc(size_t n),此时符号表按C语言修饰规则就成为了0x662521:tcmalloc,这时C程序就能正确的找到了,C++程序中有tcmaloc函数的声明,发现有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的。
总结:
当C的程序和C++的程序都想调用一个C++实现的模块时,C++可以调,但C语言不能调,那么两个都想调用时,我们就在C++实现的该模块里面函数声明时在前面加extern"C",那么生成的符号表里面就不对这个函数进行修饰了,这时C语言实现的程序就可以调用它了,紧接着C++程序在调用时,因为C++是兼容C的,发现函数的声明有extern"C",所以就在链接的时候按C的规则去找这个模块的函数
面试题:
下面两个函数能不能构成函数重载?
void TestFunc(int a = 10)
{
cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
cout<<"void TestFunc(int)"<<endl;
}
void TestRef()
{
int a = 10;
int& ra = a;
printf("%p\n", &a);
printf("%p\n", &ra);
}
不行,重载必须要函数参数列表不同,因为这样才在名字修饰时有所区别,能够区分函数。
C语言中为什么不能支持函数重载?
C编译器,直接用函数名关联,当函数名相同时,它无法区分
C++中函数重载底层是怎么处理的?
在链接阶段,有符号表的合并与重定义,那么函数名相同那么他们重载函数符号表会不会冲突呢?答案是不会的,C++有自己的名字修饰规则,比如重载函数参数类型一个为int,另一个为float,在名字修饰时,就会将这些信息代入进去,这样就区分了两个函数。
C++中能否将一个函数按照C的风格来编译?
可以,在该函数前面加extern"C"即可,C++程序中有函数的声明,发现有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的
引用
引用概念
引用并不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
类型& 引用变量名 = 引用实体;
#include<iostream>
using namespace std;
int main()
{
int a=0;
int& ra = a;
return 0;
}
我们可以重复引用,也可以对本身是引用的变量名再次进行引用,例如:
int main()
{
int a=1;
int& ra = a;
int& b = a;
int& c = b;
return 0;
}
我们随便改变一个变量都会变,因为他们共用同一块空间 :
int main()
{
int a=1;
int& ra = a;
int& b = a;
int& c = b;
b=4;
return 0;
}
我们可以进行调试观察:
发现四个变量都变成了4,并且他们共用一块空间
引用特性
#include<iostream>
using namespace std;
int main()
{
int a=0;
int& ra;
return 0;
}
这个在前面已经证实了
int main()
{
int a =1;
int& c = a;
int d =2;
c=d;
return 0;
}
我们可以看到a和c的地址是一样的,说明c没有变成d的引用,而a变成了2,说明c=d而是将d赋值给c
常引用
我们前面说的引用是这样的:
int main()
{
int a =0;
int& b=a;
return 0;
}
这样当然是可以的,但是看下面代码,这样可以吗?
int main()
{
const int a =0;
int& b=a;
return 0;
}
b的类型是int,编译不通过,原因:a是只读,b的类型是int,也就是可读可写的,变成你的别名,还能修改你,所以不行
所以需要这样写:
int main()
{
const int a =0;
const int& b=a;
return 0;
}
那么这样写可以吗?
int main()
{
int a =0;
const int& b=a;
return 0;
}
这样写是可以的,a是可读可写的,b变成别名是只读,变成a的别名,不修改你,这样是可以的。但是b是不可以改的(b是const修饰,只读不能写),但是a可以改,并不是每个别名和原名字有一样的权限
总结:
引用取别名时,变量访问的权限可以缩小,不能放大
**那么这个有什么用呢?**比如我们再学习数据结构—栈的实现时:
typedef struct Stack
{
int a[1000];
int top;
int capacity;
}ST;
void StackInit(ST& s)
{
s.top = 0;
s.capacity = 1000;
}
void PrintStack(const ST& s)
{
}
int main()
{
ST st;
StackInit(st);
PrintStack(st);
return 0;
}
我们通常是结构体传指针,因为这样可以不进行拷贝,就节省了空间的消耗,我们现在用引用也可以实现,PrintStack这个函数的实现我们不希望修改结构体成员变量,故这就用到了我们前面讲的常引用,在前面加const就不能改变结构体成员变量了,这样可以增加代码的健壮性,假设函数里面有修改结构体成员变量的操作,这里就会被检查报错,总结一下:传引用的好处是什么呢? 1、传引用是为了减少传值传参时的拷贝 2、可以保护形参,形参不会被改变
const引用做参数的第二个好处:
void func(const int& n)
{
}
int main()
{
int i = 10;
func(i);
func(20);
func(j);
return 0;
}
func该函数既可接收变量,也可以接收常量,const引用在实际当中的作用:函数传参如果想减少拷贝用引用传参,如果函数中不改变这个参数,最好用const引用传参
下面我们再来看常引用的另外的知识,首先我们看下面代码:
int i=0;
double d = i;
这样写是可以的,那么这样写可以嘛?
int i=0;
double &d = i;
float &f = i;
这样是不行的,但是加上const就可以了
int i=0;
const double &d = i;
const float &f = i;
为什么呢?要理解这个我们首先就要看一下这个赋值:
int i=0;
double d = i;
这里的赋值实际上并没有直接把i给d,隐式类型转化实际上中间产生了一个double的临时变量,i给这个临时变量,这个临时变量再给d,不光隐式类型转化产生临时变量,而且整型提升也会产生临时变量
const double &d = i;
const float &f = i;
这里就可以解释为什么加const可以,不加const不可以了,因为中间会产生一个double的临时变量,它们引用的不是i,而是引用的这个临时变量,而临时变量具有常性,而没有const时,d和f都是可读可写的,而该临时变量是只读的,故将权限放大了,所以不行,加上const,都是只读的,所以是可以的。
引用的使用场景
传参
我们之前是如何编写一个函数交换两个数的呢?我们需要传地址,用指针去接收
void Swap(int* p1,int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int x=0,y=1;
Swap(&x,&y);
return 0;
}
这是之前我们交换两个数的代码,但是有了引用之后我们可以这样写:
void Swap(int& rx,int& ry)
{
int temp = rx;
rx = ry;
ry = temp;
}
int main()
{
int x=0,y=1;
Swap(x,y);
return 0;
}
因为rx是x的引用,ry是y的引用,他们的地址是一样的,操作rx和ry就相当于操作x和y
下面我们来看一个指针变量的引用:
int main()
{
int x = 0,y = 1;
int *p1 =&x;
int *p2 =&y;
int*& p3 = p1;
*p3 = 10;
p3 =p2;
}
这是前六行代码的图解,那么第7行第8行代码执行后会是什么样子呢?
x变成10,p1和p2、p3都指向了y
返回值
首先我们先看这个代码:
传值返回:
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;
}
这里返回的不是c,是将c拷贝给临时变量,临时变量做返回值
如果比较小,通常是寄存器
如果比较大,会在main中开一块临时空间
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
const int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
这里返回的不是c,将c拷贝给临时变量,而是临时变量做返回值,而临时变量具有常性,所以接受的ret引用返回值需要加const
下面这段代码会输出什么呢?
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;
}
函数Add在返回时,int& temp = c;temp是c的别名,将c的别名temp返回(其实就是n),ret是temp的别名,即ret是c的别名,返回后第一次调用的Add函数栈帧就销毁了,但是这里销毁并不是空间不存在了,而是空间的使用权不是你的了,再次调用时,开辟相同大小的栈帧,c变成了7,又因为ret是c的别名,故ret最后打印为7,这里就算不调用Add(3,4)程序也是有问题的,结果是不确定的,因为这取决于编译器在栈帧销毁的时候会不会清理,如果恰好编译器没有清理,那么结果可能是对的,但是过程是不正确的,要是编译器清理了的话,就会是随机值
这里说明了如果返回的变量是一个局部变量时,引用返回是不安全的,因为当你调用完函数后,会销毁函数栈帧,这块空间已经不属于你了,但是你还是在通过引用能使用到,故这里发生了非法访问空间
那么这段代码有没有解决方法呢?c前面加static就可以了:
int& Add(int a, int b)
{
static 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;
}
因为在加了static后,c就不是在Add函数的栈帧建立空间了,而是在静态区建立空间
总结:
一个函数要使用引用返回,该返回变量出了这个函数的作用域还存在,就可以使用引用返回,否则返回变量会销毁,就不安全
传值、传引用效率比较
函数传参传值和传引用的效率比较
#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;
}
值和引用作为返回值类型的性能比较
#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;
}
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用一块空间,指针变量是开辟一块空间,存储变量的地址
在底层实现的角度,引用也是有空间的,因为引用是按指针的方式来实现的
指针的汇编代码:
引用的汇编代码:
引用的定义和引用访问跟指针定义和指针解引用在汇编层完全类似。
引用和指针的不同点:
-
引用在定义时必须初始化,指针没有要求 -
引用概念上定义一个变量的别名,指针存储一个变量地址 -
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体 -
没有NULL引用,但有NULL指针 -
在sizeof中含义不同:引用结果为引用类型的大小(与类型有关),但指针始终是地址空间所占字节个数(32位平台下占4个字节)(与类型无关)
int main()
{
double a = 10;
double* pa = &a;
*pa = 20;
double& ra = a;
ra = 20;
printf("指针pa的大小:%d\n",sizeof(pa));
printf("引用ra的大小:%d\n", sizeof(ra));
return 0;
}
内联函数
假设swap这样的函数被频繁调用,能否有什么优化的方法?用法不变,不建立栈帧呢?
C语言的方法是什么呢?宏
宏这个东西虽然很有价值,但是他有不少缺点
1、语法复杂,主义的细节多,容易出错
2、没有类型安全的检查,在预处理阶段已经替换
3、不能调试
又有些东西来替代宏:推荐用const、enum、inline替代宏
概念
如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字inline ,在调用函数之前需要对函数进行定义。
inline int Add(int x,int y)
{
return x+y;
}
int main()
{
Add(2,3);
return 0;
}
我们可以看到汇编代码中,没有Add函数的call
特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
普通函数在链接阶段找函数定义,而内联是在编译阶段找函数定义,如果内联函数定义定义在头文件中,所有包含该头文件的编译单元都可以正确找到函数定义,然而,如果内联函数定义在编译单元A中,那么在其他编译单元中调用fun()的地方将无法解析该符号,因为编译单元A生成目标文件A.obj,内联函数fun()已经被替换掉,A.obj中不再有fun这个符号,链接器自然无法解析,所以,如果一个内联函数会在多个源文件当中用到,那么必须定义在头文件中
auto关键字(C++11)
auto的使用细则
int main()
{
int a =1;
char b='a';
auto c = a;
auto d = b;
return 0;
}
类型太复杂,太长,auto自动推导变量类型,简化代码
缺点:一定程度上牺牲了代码的可读性
typeid可以去看变量的实际类型
cout<<typeid(c).name()<<endl;
cout<<typeid(d).name()<<endl;
下面再看一个程序:
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
auto不能推导的场景
void TestAuto(auto a)
{}
只有赋值才能推导出类型
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
C++98auto作用反正也没价值,所以C++只保留了auto作为类型指示符的用法
基于范围的for循环(C++11)
范围for的语法
C++11提供一种新的访问数组的方式
int main()
{
int array[]={1,2,3,4,5};
for(auto e:array)
{
cout<<e<<" ";
}
cout<<endl;
}
上面是打印,那么我们能改变数组吗?
答案是可以的,但是我们e需要是引用,因为需要修改数组需要修改数组本身
int main()
{
int array[]={1,2,3,4,5};
for(auto& e:array)
{
e*=2;
}
for(auto e:array)
{
cout<<e<<" ";
}
cout<<endl;
}
范围for的使用条件
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
int main()
{
int array[]={1,2,3,4,5};
TestFor(array);
return 0;
}
范围for看起来很厉害,实际没什么东西,都是替换成普通访问了
指针空值—nullptr(C++11)
C++98中的指针空值
我们在定义一个指针变量时,都需要给它进行初始化,否则可能会造成野指针的问题,在C++98中初始化指针:
int* p1 = NULL;
而C++11中初始化指针:
int* p2 = nullptr;
指针本质是内存按字节为单位空间的编号,空指针并不是不存在的指针,而是内存第一个字节的编号,一般我们不使用这个字节存有效数据,用空指针一般用来初始化,表示指针指向没有存储一块有效数据的空间
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)
C++11中,所以我们可以这样:
f(nullptr);
这时就调用了f(int*)函数
欢迎大家评论区学习交流!!!
|