C++声明浅析
明天就要考C++了,赶紧复习一下C++。
变量#
int a;
a是一个整型变量,可以在声明时同时给定初值int a=1;
.
其中全局变量若不显示指定初值,会自动初始化为0.
而局部变量若不初始化,值是任意的。
常量#
const int a=1;
a是一个整型常量,声明时必须给定初值(可以是字面量、常量或者变量),声明后a的值不会改变.
也可写成int const a=1;
,两种写法没有区别.
指针#
int a=1;
int *b;
b=&a;
b是一个整型指针,指向一个整型变量,b中存储的值为指向整型变量的地址.
无论指针指向的变量是什么类型,指针所占用的内存空间一般是固定的.
注意int* a,b;
声明的a是指针,b是变量。
指针的指针(二级指针)#
int a;
int *b=&a;
int **c=&b;
c是一个二级整型指针,指向一个整型指针b,b指向整型变量a.
注意不能写成int **c=&&a;
依次类推有三级指针、多级指针.
常量指针(指向常量的指针)#
const int c=1;
const int d=1;
const int *b;
b=&c;
b=&d;
上面b的声明也可以写成int const *b;
但不能写成const int const *b;
.
b是一个指向整型常量的指针,可以用整型常量的地址给b复制,b的值可以改变,不能通过*b
改变指向常量的值.
(但由于b的值可变,*b
的值也可能变化)
注意:指向常量的指针说法不一定准确,如下面的声明也是合法的:
int a=1;
const int *b=&a;
此时,b是一个指向整型变量的指针,a、b的值均可变,但不能通过*b
来改变a的值.
常指针(指针常量)#
int a=1;
int *const b=&a;
*b=2;
b是一个整型常指针,声明时必须用整型变量的地址赋初值,声明后b的值不可改变,但可以通过*b
改变指向变量的值.
指向常量的常指针(常指针常量)#
const int a=1;
const int *const b=&a;
b是一个指向整型常量的常指针,同时具备上述两种特性.
同样可以写成int const *const b=&a;
但不存在const int const *const b=&a;
的写法.
注:由于指向常量的指针和常指针声明类似(仅仅const位置不同)容易混淆,记录一种(来源网上)的方法记忆.
先忽略类型关键字(如上面的int),然后看const修饰的是谁,谁就不可变.
如const int *p;
const修饰的是*p
,则*p
不可变(实际若p可变的话*p
也会变),那么p是指向常量的指针.
如const int * const p;
const同时修饰p和*p
,则p和*p
不可变,那么p是指向常量的常指针.
至于const int const *p;
忽略int后两个const重复,显然声明有问题.
引用#
int a=1;
int &b=a;
int &c=b;
int *d=new int;
int &e=*d;
b是一个整型引用,声明时必须初始化,可以用变量,也可以用变量引用、或地址对应变量初始化,但不能用常量初始化.
可以把b看作a的一个别名,对b的操作就是对a的操作,b的内存地址和a的内存地址相同.
引用常常用在函数参数的传递过程中,若是希望能修改参数的值或是减少参数复制带来的开销,一般用引用传参.
引用应该也是通过常指针来实现的.
常引用(常量引用)#
int a=1;
const int &b=a;
b是一个整型常引用,声明时必须初始化,此处用变量初始化,与引用类似,b的内存地址和a的内存地址相同,只不过不能通过b修改a的值,但a的值可以修改(从而b的值也可变),同样可以写成int const &b=a;
.(类似于常量指针)
const int a=1;
const int &b=a;
b还可以用常量初始化,此时b的内存地址和a的内存地址相同,但由于a是常量不可变,b的值也不可变.(类似于常指针常量)
const int &b=1;
b甚至还可以用字面量初始化,此时会隐含创建一个匿名常量,再把b作为匿名常量的引用,b的值显然不可变.
由此就不难理解下面的声明语句。
int a=1;
const int &b=a+1;
const int &c=a+a;
其中a+1和a+a都可以看作常量(因为隐含创建的变量是匿名的),b、c和a的内存地址都不同,即使a的值改变了,b、c的值也不会变化.
与引用类似,常引用也可用常引用初始化.
int a=1;
const int &b=a;
const int &c=b;
常引用常常用在函数参数的传递过程中,若是希望能减少参数复制带来的开销同时不修改参数的值,一般用常引用传参.
引用常量#
int a=1;
int & const b=a;
上述写法是错误的,一般编译器会报错,但在VS中可以通过编译,&
后的const会被忽略.
因此可以说不存在引用常量.
指针的引用#
int *b;
int *&d=b;
与引用类似,d可看作是b的别名.
注意不可写成:
int a;
int *&b=&a;
因为&a
是常指针,而b是普通引用,可以通过常指针引用解决这个问题.
常量指针的引用#
const int *b;
const int *&c=b;
可看作常量指针的别名.
注意c不是指针的常引用,可以通过c更改b的值.
常指针的引用#
int a=1;
int * const b=&a;
int * const &c=b;
c是一个常指针引用,类似常指针的别名.
再如指针的引用时提到的问题:
int a;
int * const &b=&a;
上述写法可以.
常指针常量的引用#
int a=1;
const int * const b=&a;
const int * const &d=b;
类似常指针常量的别名.
常量指针的常引用#
注意到由于常量指针的引用声明时const已经占据了常引用声明时const的位置,那如何声明一个常量指针的常引用?
先来看常量指针的引用的情况:
int a=1;
int d=2;
const int *b=&a;
const int *&c=b;
c=&d;
此时可通过c改变b的值,显然c不是常引用.
方法一:typedef#
利用typedef把常量指针包装成一个新的类型,就不会占据const位置了.
int a=1;
int d=2;
const int *b=&a;
typedef const int *pci;
const pci &c=b;
//c=&d;
此时就不能用c改变b的值了,c是常量指针的常引用.
方法二:auto#
int a=1;
int d=2;
const int *b=&a;
const auto &c=b;
//c=&d;
c被自动推断为b类型的常引用,而b是常量指针.
方法三:decltype#
int a=1;
int d=2;
const int *b=&a;
const decltype(b) &c=b;
//c=&d;
利用decltype(b)
获取b的类型.
指针的常引用#
上述方法具有一般性,指针的常引用也可如此声明.
常量的常引用#
常指针的常引用#
常指针常量的常引用#
由于上述三者本身不可变,故声明其对应的常引用没有意义.
可以仿照常量指针的常引用的声明方法进行声明,不会报错.
数组#
int a[3];
a是一个含3个整型变量(从a[0]~a[2]
)的整型数组,可以在声明时同时初始化,如int a[3]={1,2,3};
即表示a[0]=1,a[1]=2,a[2]=3,若初始化的值个数少于数组总元素个数,后面的值会被初始化为0.
当有初始化时,数组个数可以省略,由编译器根据初始化元素自动判断数组大小,如int a[]={1,2,3,4}
.
与变量类似,全局数组会自动初始化,局部数组不会,但通过int a[105]={0};
即可全部初始化为0.
一般而言,数组申明是若未初始化,需要用常量指定数组大小。但在一些较新的编译器里已经支持(VS会报错)用变量定义数组.
如以下代码不会报错:
int n;
cin>>n;
int a[n];
但极不推荐这样做,若要使用动态数组可以用new申请空间或是用STL中的vector容器,而且这种用法可能存在潜在的问题.
a可以看做是常指针类型(int * const
),a的地址就是数组首地址&a[0]
,但数组首地址对应的值为a[0],a中的值也是数组首地址(a的值与a的地址相同,这一点与指针不同),a+1是&a[1]
,但要注意a实质为一个数组类型,如sizeof(a)=12
,&a+1
会跨越一个数组的长度,此时可以看作a的类型为int[3]
.
值得一提的是,当数组名作为函数形参时,会被’降维’处理,不再具备int[3]
的性质,而彻底变成了int *
类型(可以修改,sizeof为指针大小).
二维数组#
int a[2][3];
a是一个含6个整型变量的二维整型数组(从a[0][0]~a[1][2]
),可以在声明时同时初始化,如int a[2][3]={{1,2,3},{4,5,6}};
即表示a[0][0]=1,a[0][1]=2,a[0][2]=3,a[1][0]=4,a[1][1]=5,a[1][2]=6
,同样,若初始化的值个数少于数组总元素个数,后面的值会被初始化为0.
与一维数组不同,二维数组也可对每行的前面部分初始化,如int a[2][3]={{1,2}, {4}};
表示a[0][0]=1,a[0][1]=2,a[0][2]=0,a[1][0]=4,a[1][1]=0,a[1][2]=0
.同时还支持线性初始化,如用int a[2][3]={1,2,0,4};
初始化结果与上面相同.
但是,初始化时仅能省略第一维的大小,其余维大小必须指明,例如上面的声明语句还可写成int a[][3]={1,2,3,4};
,第一维大小被自动判断为2.
二维数组与指针的关系就更加复杂了,仿照一维数组,此时a可以看作是数组常指针int (*const)[3]
类型,&a[0][0]=a[0]=&a[0]=a=&a
,同样a实质为int[2][3]
类型,a+1=&a[1][0]
,sizeof(a)=24
,由此不难理解一些用指针处理数组的方法,如:(n,m为整型常量)
int a[n][m];
a[i][j]=*(a[i]+j)=*(*(a+i)+j)=*(*a+i*m+j)=*(a[0]+i*m+j)=a[0][i*m+j]=*(*(&a[0]+i)+j)=*(&a[0][0]+i*m+j)
由此也可以发现,在用指针处理高维数组时,第一维的大小相对而言不那么重要.
一维数组时提到可以用变量作为数组大小申明,二维数组其实也可以,但仍然可能有问题,如下面这个例子:
int n=2,m=3;
int a[n][m]={1,2,3,4,5,6};
输出a中元素会发现a[1][0]=a[1][1]=a[1][2]=0,而若第一行改成const int n=2,m=3;
就不会有这个问题.
类比一维数组,二维数组在作为函数形参时,也会被’降维’处理,(即使函数参数写成int[2][3]
,第一维可省略),不再具备int[2][3]
的性质,而彻底变成了int (*)[3]
类型(可以修改,sizeof为指针大小).
常量数组#
const int a[3]={1,2,3};
a是一个含3个整型常量的常量数组,声明时必须初始化,但可以初始化前面一部分,如const int a[3]={1,2};
剩下的会自动初始化为0,数组大小也可以省略不写.
常用在误差表的处理或简化判断语句。
常量二维数组#
const int a[2][3]={1,2,3,4,5,6};
与二维数组和常量数组具有类似的性质.
指针数组#
int *a[3];
与数组类似,元素为指针类型.
注意此时[]的优先级较高,先与a结合,说明a是一个数组.
指针二维数组#
int *a[2][3];
与指针数组和二维数组有类似的性质.
数组指针(指向一维数组的指针、行指针)#
int a[3]={1,2,3};
int (*b)[3]=&a;
数组指针实质为指针,指向一个一维数组.
此时()
优先级更高,先与b结合,b是一个指针.
数组指针中数组大小不能省略.
数组常指针#
int a[3]={1,2,3};
int (*const b)[3]=&a;
声明时必须初始化,与二维数组名类似.
常数组指针#
const int a[3]={1,2,3};
const int (*b)[3]=&a;
常数组常指针#
const int a[3]={1,2,3};
const int (* const b)[3]=&a;
二维数组指针#
int a[][3]={1,2,3,4,5,6};
int (*b)[2][3]=&a;
同样b是指针,大小均不能省略.
二维数组常指针#
int a[2][3]={1,2,3};
int (*const b)[2][3]=&a;
常二维数组常指针#
const int a[2][3]={1,2,3,4,5,6};
const int (* const b)[2][3]=&a;
引用数组#
下面语句试图申明元素为引用的数组,会报错.
int a[3]={1,2,3};
int &b[3]={a[0],a[1],a[2]};
因此可以说不存在引用数组.
数组引用#
类比数组指针有:
int a[3]={1,2,3};
int (&b)[3]=a;
指针数组引用#
int* a[3];
int* (&b)[3]=a;
a是指针数组,b是指针数组的引用.
数组指针引用#
int a[3]={1,2,3};
int (*b)[3]=&a;
int (*&c)[3]=b;
a是数组,b是数组指针,c是数组指针的引用.
二维数组引用#
int a[2][3]={1,2,3,4,5,6};
int (&b)[2][3]=a;
数组常引用#
int a[3]={1,2,3};
const int (&b)[3]=a;
常数组引用#
与数组常引用相同.
const int a[3]={1,2,3};
const int (&b)[3]=a;
函数#
void func();
func的地址就是函数首地址,(函数首地址对应的值也是函数首地址)func的值也是函数首地址,比数组名更特殊,因此可以对函数名无限次间值运算仍然正确.(***************func)();
可以通过编译,而且函数运行正常!
函数指针#
int func(int x){return 0;}
int (*pfunc)(int)=func;
与函数名类似,但指针可变,函数名不可变.
指针函数(返回值为指针的函数)#
int* func(int x){return nullptr;}
函数常指针#
int func(int x){return 0;}
int (* const pfunc)(int)=func;
函数引用#
int func(int x){return 0;}
int (&pfunc)(int)=func;
函数指针数组#
int (*pfunc[3])(int);
函数指针数组引用#
int (*pfunc[3])(int);
int (*(&fun)[3])(int)=pfunc;
pfunc是函数指针数组,fun是函数指针数组引用.
引用的引用(右值引用)#
注意并不是说引用的引用又称为右值引用,只是放在一块记录.
在说明引用时,提到可以用引用来初始化,这样也算一种引用的引用,但实质还是原始变量的引用.
int a=1;
int &b=a;
int &c=b;
如c是a的引用的引用.
但下面的代码是错误的:(试图声明c为a引用的引用)
int a=1;
int &&c=a;
&&
是用来声明右值引用的,其中涉及到深浅复制、移动构造函数、移动语义,可以参考这篇文章:从4行代码看右值引用
上述都是一些基本的类型申明,事实上还有许多复杂的类型声明,虽然一般不会用到,但理解一下有助于开阔思维。
Keep It Simple & Stupid
下面列举几个较为复杂的例子,可以参考网上的’右左法则’理解这些声明.(先右后左,遇括号反向)
参数为函数指针的函数#
int func(int (*)(int));
func是一个函数,返回值为int,有一个参数,该参数是一个指针,指向一个函数,被指向的函数返回值为int,有一个参数int.
返回值为函数指针的函数#
int (*fun(int))(int *);
fun是一个函数,有一个int型参数,返回值为指针,指向一个函数,被指向的函数返回值为int,有一个int型指针参数.
const叠加二级指针#
const char *p1;
const char * const * const p2=&p1;
p1是一个常量指针,p2是一个指向常量指针的常指针常量.
最后用一个声明结束本文,赶紧复习英语.
char*const*(*(*tootal())[520])(int **(&&),double *(&)[666],void*&());
Eternity is a very long time, especially towards the end.
永恒是很长的时间,特别是对尽头而言。