C++基础
1.C++ 关键字(98版,63个)
1.1 C++ 关键字表
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++98有63个关键字,C语言有32个关键字
C++11有73个关键字(新增alignas、alignof、char16_t、char32_t、constexpr、decltype、noexcept、nullptr、static_assert、thread_local)
C++兼容C语言头文件,也有自己的用C库的方法:stdio.h(C/C++)、cstdio(C++)
2.C++ 命名空间
2.1 命名空间的常见形式
C++中定义命名空间使用namespace关键字,一般写法为namespace+命名空间名字+{}—命名空间解决了命名空间污染问题
C++命名空间的三种情况:
- 单独命名空间
- 嵌套命名空间
- 同名命名空间会合并
namespace A
{
int num;
int ADD(int num1,int num2)
{
return num1+nunm2;
}
}
namespace B
{
int num;
int ADD(int a,int b)
{
return a+b;
}
namespace C
{
int num;
int ADD(int aa,int bb)
{
return aa+bb;
}
}
}
namespace A
{
int num;
}
namespace A
{
int ADD(int num1,int num2)
{
return num1+num2;
}
}
2.2 命名空间的常见使用方法
命名空间一般有三种使用方法:
- 命名空间名+: :
- using+命名空间名;
- using namespace+命名空间名;
namespace A
{
int num=1;
int num2=2;
int ADD(int x,int y)
{
return x+y;
}
}
int main()
{
cin>>A::num>>A::num2;
return 0;
}
namespace A
{
int num=1;
int num2=2;
int ADD(int x,int y)
{
return x+y;
}
}
using A::num;
int main()
{
cin>>num;
cin>>A::num2;
return 0;
}
namespace A
{
int num=1;
int num2=2;
int ADD(int x,int y)
{
return x+y;
}
}
using namespace A;
int main()
{
cin>>num>>num2;
ADD(3,4);
return 0;
}
using可以理解为释放的意思,比如using A::num;就是释放A中num但是不释放num2和ADD,意思就是num2和ADD必须在前面加A: :才能正常使用,而num可以直接用!
3.C++ 输入与输出
3.1 cout与cin标识符
C++中的输入与输出是使用头文件和std标准命名空间实现的
我们使用标准化输出依靠cout和cin标识符。cout标准输出(控制台)、cin标准输入(键盘)
#include<iostream>
using namespace std;
int main()
{
int a;
float b;
cin>>a>>b;
cout<<a<<b<<endl;
cout<<a<<b<<'\n';
}
4.C++ 缺省参数/默认参数
4.1 缺省参数概念
缺省参数(也称默认参数)是声明或定义函数时,为函数的参数指定一个默认值
void func(int x=0)
{
cout<<x<<endl;
}
int main()
{
func();
func(1);
}
4.2 缺省参数类型
缺省参数分为:全缺省参数、半缺省参数
缺省参数特性:
1.缺省参数不能在声明和定义中同时出现,声明和定义二选一写- - -否则报错:重定义默认参数- - -原因:怕你两个默认参数给的不一样
2.半缺省参数必须从右往左依次给出,不能间隔给,否则编译器不知道识别谁
3.缺省参数值必须是常量或全局变量
4.C语言不支持缺省参数,C++支持缺省参数
void func(int x=0,int y=1,int z=2)
{
cout<<"x="<<x<<endl;
cout<<"y="<<y<<endl;
cout<<"z="<<z<<endl;
}
void func2(int x,int y=1,int z=2)
{
cout<<"x="<<x<<endl;
cout<<"y="<<y<<endl;
cout<<"z="<<z<<endl;
}
5.C++ 函数重载
5.1 函数重载的概念
函数重载:在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表必须不同
函数重载特性:函数名相同,参数不同(参数的个数、类型、顺序不同),与返回值类型无关
int Add(int x,int y)
{
return x+y;
}
double Add(double x,double y)
{
return x+y;
}
long Add(long x,long y)
{
return x+y;
}
5.2 函数名字修饰原则
实际上我们的项目通常是由多个头文件和多个源文件构成的,通过编译链接我们可以知道:
当两个cpp文件(一个函数在x.cpp中声明,y.cpp中定义)中定义一个Add函数时,我们编译后链接前,x.o的目标文件中没有Add函数的地址,因为Add是在x.cpp中声明,y.cpp中定义的,所以Add的地址在y.o中!==因此链接阶段就是专门处理这个问题的,链接器看到x.o调用了Add,但是没有Add的地址,就会到y.o的符号表中找到Add的地址,然后链接在一起!==因此我们需要为函数名字拟定一个修饰规则
Linux下gcc、g++的修饰规则: ①gcc函数修饰名字不变 ②g++函数修饰为_Z+函数长度+函数名+类型首字母
比如gcc我们定义一个int f(int x,int y)—> f
比如g++我们定义一个int Add(int x,int y)—> _Z3Addii
比如g++我们定义一个int Add(double x,double y)—> _Z3Adddd
如图,在Linux下采用gcc编译后,函数名字的修饰没有发生改变,函数名后的数据是函数参数类型的信息
如图,在LInux下采用g++编译后,函数名字修饰发生改变,函数名后的数据是函数参数类型的信息
windows下名字修饰规则:根据编译器规则
从以上说明就可以基本理解,经常说的C语言不支持重载,而C++支持重载的原因是:g++编译后函数名字修饰改变了,同时也说明了为什么函数重载要求参数不同,而跟返回值无关的原因
C++通过函数修饰规则来区分函数重载,只要参数不同,修饰出来的名字不同,就支持了重载!而C语言无法识别出来!
6.C++ 引用
6.1 引用的概念
引用不是新定义一个变量,而是取一个别名,编译器不会引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
引用类型和引用实体的类型必须相同
void func()
{
int x=0;
int& tmp=x;
printf("%p\n",&x);
printf("%p\n",&tmp);
}
6.2 引用的特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.一旦引用一个实体,不能再引用其他实体
4.引用只能缩小权限不能放大权限,及引用的权限范围小于或者等于引用实体
void func()
{
int x=0;
int& num1=x;
int& num2=x;
printf("%p %p %p\n",x,num1,num2);
}
6.3 常引用
常引用涉及const权限的放大与缩小问题
void func()
{
const int x=0;
const int& tmp=x;
const int& num=0;
double y=1.234;
const int& num2=y;
int f=y;
}
使用场景:①做参数 ②做返回值
void Swap(int& x,int& y)
{
int temp=x;
x=y;
y=temp;
}
int& count()
{
static int n=0;
n++;
return n;
}
注:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,必须使用传值返回(即变量出了函数还在吗,在就用引用,不在就用传值)
传值、传引用效率比较: 以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低
#include<iostream>
using namespace std;
#include<time.h>
struct A
{
int a[10000];
};
void func1(A a){}
void func2(A& a){}
void func()
{
A a;
size_t begin1=clock();
for(size_t i=0;i<10000;++i)
func1(a);
size_t end1=clock();
size_t begin2=clock();
for(size_t i=0;i<10000;++i)
func2(a);
size_t end2=clock();
cout<<"func1(A)-time:"<<end1-begin1<<endl;
cout<<"func2(A)-time:"<<end2-begin2<<endl;
}
int main()
{
func();
return 0;
}
值和引用的作为返回值类型的性能比较:
#include<iostream>
#include<time.h>
using namespace std;
struct A
{
int a[10000];
};
A a;
A func1()
{
return a;
}
A& func2()
{
return a;
}
void func()
{
size_t begin1=clock();
for(size_t i=0;i<10000;++i)
func1();
size_t end1=clock();
size_t begin2=clock();
for(size_t i=0;i<10000;++i)
func2();
size_t end2=clock();
cout<<"func1 time:"<<end1-begin1<<endl;
cout<<"func2 time:"<<end2-begin2<<endl;
}
int main()
{
func();
return 0;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大,传引用远优于传值
传值与传引用返回总结:
- 传值返回:会有临时拷贝
- 传引用返回:没有临时拷贝,函数返回的直接就是返回变量的别名
- 反正记住返回的变量是static才用传引用返回,否则可能出现越界问题
6.4 引用与指针的区别
引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
在底层实现上实际是有空间的,因为引用是按照指针的方式来实现的
指针和引用的不同点:
1.引用在定义时必须初始化,指针无要求
2.引用在初始化引用一个实体后,无法再引用其他实体,而指针无要求
3.没有NULL引用,有NULL指针
4.在sizeof中含义不同:引用结果为引用类型的大小,指针始终是地址空间所占字节个数
5.引用自加则实体增加1,指针自加则指针向后偏移一个类型大小
6.有多级指针,但是没有多级引用
7.访问实体方法不同,指针需要显示解引用,引用编译器自己处理
8.引用比指针使用更安全
7.C++ 内联函数
7.1 内联函数概念
以inline修饰的函数叫做内联函数,编译时C++编译器会把调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率
内联函数不放入符号表
函数展开也就是说编译器将指定的函数体插入并取代每一处调用该函数的地方,相当于直接把内容写在了调用的地方
7.2 内联函数的查看方法
方法一:在release模式下,查看编译器生成的Add函数汇编代码中是否存在call Add
方法二:在debug模式下,需要对编译器设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)
7.3 内联函数的特性
1.inline是一种以空间换时间的方法,省去函数压栈开销
2.inline不适合代码很长或者有循环、递归的函数(10行以内适合用)
3.inline对于编译器而言只是一个建议,编译器会自动优化
4.inline不建议声明和定义分离,会发生链接错误(inline被展开,它不进入符号表,就没有函数地址了,链接找不到)
#include<iostream>
using namespace std;
inline void func(int x);
#include"demo.h"
void func(int x)
{
cout<<x<<endl;
}
#include"demo.h"
int main()
{
func(10);
return 0;
}
8.C++ auto关键字(C++11)
8.1 auto关键字的概念
1.个人觉得,auto很少有人使用,只有后面用模板的时候才会用到
2.在早期C/C++中auto时含义:使用auto修饰的变量,是具有自动存储器的局部变量
3.C++11中,标准委员会重新赋予了auto含义:auto作为新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
简单来说:auto自动匹配数据类型
int func()
{
return 0;
}
int main()
{
int x=1;
auto xx=x;
auto xxx='x';
auto xxxx=func();
cout<<typied(xx).name<<endl;
cout<<typied(xxx).name<<endl;
cout<<tupied(xxxx).name<<endl;
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型
8.2 auto使用规则
1.auto声明指针类型时,auto与auto*没有区别
2.auto声明引用类型时,必须加&
3.auto声明多个变量,类型必须相同—auto识别第一个变量,后面也认为时这个变量
4.auto不能作为函数参数
5.auto不能声明数组
1.auto与指针和引用结合使用
1.用auto声明指针类型时,用auto和auto*没有任何区别
2.用auto声明引用类型时必须加&
int main()
{
int x=0;
auto xx=&x;
auto* xxx=&x;
auto& xxxx=x;
cout<<typeid(xx).name<<endl;
cout<<typeid(xxx).name<<endl;
cout<<typeid(xxxx).name<<endl;
*xx=1;
*xxx=2;
xxxx=3;
return 0;
}
2.在同一行定义多个变量
1.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错
2.编译器只对第一个类型进行推导,然后用推导出来的类型定义其他变量
{
auto a=1,b=2;
auto c=3,d=4.0;
}
3.auto不能作为函数的参数
void func(auto a)
{
return a;
}
4.auto不能直接用来声明数组
void func()
{
int a[]={1,2,3,4,5,6};
auto b[]={1,2,3,4,5};
return 0;
}
补充:
? ①为了避免和C++98中的auto发生混洗,C++11只保留了auto作为类型指示符的用法
? ②auto在实际中最常见的优势用法就是跟以后会讲到的C++提供的新式for循环,还有lambda表达式等进行配合使用
9.C++ 基于范围的for循环(C++11)
9.1 for遍历与范围for遍历
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
void func()
{
int a[]={1,2,3,4,5,6,7};
for(int i=0;i<sizeof(a)/sizeof(a[0]);++i)
a[i]+=1;
for(int* j=a;j<a+sizeof(a)/sizeof(a[0]),++j)
cout<<*j<<endl;
}
void func()
{
int a[]={1,2,3,4,5,6,7};
int aa[]={2,4,6,8,10,12};
for(auto e:a)
e/=2;
for(auto e:a)
cout<<e<<" ";
for(auto&ee :aa)
ee/=2;
for(auto& ee:aa)
cout<<ee<<" ";
return 0;
}
9.2 范围for的使用要求
1.for循环迭代的范围必须是确定的
void func(int a[])
{
for(auto& e:a)
cout<<e<<endl;
}
10.C++ 指针空值nullptr(C++11)
10.1 指针空值的底层原理
指针空值的初始化:
void Ptr()
{
int* p1=NULL;
int* p2=nullptr;
int* p3=0;
}
NULL底层就是一个宏,值为0,在传统的C头文件stddef.h中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量---NULL和0的值一样就会引发问题--所以C++使用了nullptr
指针空值的规则:
- 为了提高代码的健壮性,在C++11后表示指针空值时建议最好使用nullptr
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr时C++11作为关键字引入的
- 在C++11中,sizeof(nullptr)与sizeof((void *)0)所占的字节数相同
11.剖析extern “C”
曾经Google有一个项目叫tcmalloc和tcfree项目,就是用C++写tcmalloc和tcfree两个接口来实现更高的效率,但是写出来的话C语言就没办法使用,于是就诞生了extern "C"这个东西
extern "C"的主要作用是:为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名
11.1 解析extern “C”
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif
extern “C” 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。被extern "C"限定的函数或变量是extern类型的。
11.2 extern "C"的应用场景
1.C++代码调用C语言代码、在C++的头文件中使用
在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为Example.h)时,需进行下列处理:
extern "C"
{
#include "Example.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
#include "cExample.h"
int Add( int x, int y )
{
return x + y;
}
extern "C"
{
#include "Example.h"
}
int main(int argc, char* argv[])
{
Add(2,3);
return 0;
}
在C中引用C++语言中的函数和变量时,C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int Add( int x, int y );
#endif
#include "cppExample.h"
int Add( int x, int y )
{
return x + y;
}
extern int Add( int x, int y );
int main( int argc, char* argv[] )
{
Add( 2, 3 );
return 0;
}
11.3 extern "C"引出的宏问题
宏的优点:
? ①增强代码的复用性
? ②提高性能
宏的缺点:
? ①不方便调试宏(因为预编译阶段进行了替换,不好读)
? ②导致代码可读性差,可维护性差,容易误用
? ③没有类型安全的检查
C++替换宏的技术方法:
? ①C++使用常量定义换用const
? ②C++函数定义换用内联函数
|