关于C++的几个有趣的坑

来源:互联网 发布:淘宝卖家怎么关闭订单 编辑:程序博客网 时间:2024/06/03 23:47

C++ 语言


我先学的java,现在再来学C++,发现他们还是有很多不同的,我在翻各位大佬的博客时,发现有好多问题说的也不是很清楚,那么今天就来谈谈我在复习C++时的几个有趣问题的理解,错误之处请多多指教。
前几天看到这么个题,double (*a[])(int );,问这个声明是什么,乍一看,还真是有点懵,查了一些资料发现是关于函数的复杂声明;那么今天就从这个问题入手。
一.函数的复杂声明
主要规则
1. 从未声明的标识符开始分析,()优先级最高,其次[],最后*
2. 声明中若有多个标识符,从左向右,最左的是未声明的
3. 弄清楚标识符对应的类型,若与标识符第一个结合的是*,则就是指向…的指针,若是[],则是…的数组,若是(),则是返回…的函数
4. 对于const,若后面是类型区分符(int等),则其作用于类型区分符,否则作用于左侧紧邻 * 指针运算符,注意,若左侧没有指针运算符,则其作用于类型区分符,即int const *p 和 const int *p一样。


下面看几个例子:
对于开始我们说的那个例子,运用我所说的规则,最左边的标识符是a,结合性最高的是[],他是一个数组,结合性由高到低依次分析,数组的每一个元素都是指针,这个指针指向一个函数,这个函数返回值是double类型,他的参数是int型。

const int *const (*f())[];

上面这个声明:第一个标识符是f,f是一个函数,这个函数的返回值是一个指针,这个指针指向一个数组,const后不是类型区分符,与前面指针结合,,最左边const右边是int ,这个数组里是常量指针这个指针指向const int 类型变量

int (*(*p(int a))(int a,int b))(int a);

第一个未声明的标识符p,p是一个函数,这个函数的参数是一个int型形参,这个函数返回一个指针,这个指针指向一个两个参数的函数,这个两个参数的函数返回一个指针,这个指针指向的又是一个函数,这个函数有一个int参数,最后,函数p返回的指针指向的函数返回的指针再指向的函数返回一个int类型的值

二,递增和递减运算符
说这个问题之前,最好了解一下左值右值,副作用和顺序点的概念,看了好多博客,那种繁杂的定义我就不说了,我的最简单的理解是:
左值可以对变量赋值(非const),右表示的是一个值,不能改变这个值,也不能对其取地址,

规则:1.递增递减运算符的操作数必须是可修改的左值
2.i++返回结果是其操作数的值,为右值,++i是先将操作数加一再使用,为左值
3.i++优先于++i*


++(i++)    (++i)++   i++++  ++i++   i+++++j

对于第一个,i++返回右值,而我们要求操作数必须为左值,所以第一个表达式是错误的,同理第二个是正确的,第三个是错误的,由于i++优先级高,所以第四个与第一个是一样的,错误。最后一个,编译器采用的是一种贪心算法,他会按照词法元素的最长字符序列读入,所以最后一个等同于((i++)++)+j

三.指针与二维数组
首先澄清几个概念:
数组名表示的是数组的首地址,对于一维数组,这个学计算机的都知道== ,但是我还是要强调一下,这里的数组名指的是数组中第一个元素的地址,比如int a[10];a指向的是地址&a[0],表示整个数组的地址是&a,&a表示的是一个包含有10个元素的一维数组的地址,虽然这里a和&a的地址值是相同的,但是他们的类型是不同的,a的类型的指向int的指针,&a是指向一个11个元素的一维数组的指针 ,这个对于我们后面的分析很重要。


进一步理解数组名:
关键是要明白数组名所表示的指针类型
重要规则:数组名是指向第一个元素地址的指针,对于N维数组,这个指针的类型是指向N-1维数组的指针
int a[2][3] a指向第一个元素a[0]的指针,即a指向的地址是&a[0],这个元素是一个有三个元素的一维数组,因此a是指向三个元素数组的数组指针,即与(*p)[3]的类型相同
对数组名取地址的理解:
对N维数组取地址,类型为指向N位数组的指针

int a[2][2][2];则,&a的类型为 指向三维数组的指针

char **p1;   char *p2[2];  char p3[2][3];  char (*p4)[3];

对于上面这四个式子,前面两个的p类型是一样的,后面两个的p类型的一样的,对于第一个,是一个二级指针,也可以理解为一个指向char 类型的指针,p2指向p2[0],即p2为指向char 类型的指针,所以这两个是同类型的,可以相互赋值,p3[2][3]是一个二维数组,即p3为指向一个有三个元素的数组的指针,p4很好理解,是一个指向三个元素数组的指针,所以后面两个p的类型相同,p1=p3;这种写法是错误的。

p2[0] = new char[6];  p3[0] = new char[6];

这两种写法只有一个是正确的,是哪个呢?答案是第一个,注意,对于p2[0]来说,他是一个左值,是char *类型,而对于p3[0],他是一个常量,是指向三个元素的地址,不能出现在左边。
四,引用与指针
这个问题困扰了我好久,受java的影响,我对这个引用一直是情有独钟,虽然记住书上的结论对于平常使用已经足够了,但我还是一直想弄清楚他底层到底是个什么东西,毕竟他在一些特性上表现的是很奇怪。
对于C++这个指针,总是能看到汇编语言的影子(最近学了计组,略微了解==),他怎么就那么像汇编的那个[],看了几位大佬用汇编程序来解释引用与指针的关系,我这里就不写了。
先看一段程序

void main(){    int x = 2;    int &y = x;    printf("&x=%x,&y=%x\n",&x,&y);}

输出结果&x &y的值是一样的,b的地址我们没法获得,C++没有提供这样的函数,追踪到问题的根源,其实是编译器对这个所谓的引用做了手脚,编译器会将&y解释 &(*y)=&x,所以从底层实现来看,引用变量是存放的是被引用变量的地址,但是对于我们高级语言程序员来说,编译器为我们把指针与引用的区别屏蔽了。下面说干货:
从底层实现来说,引用的实质就只指针,但是,从高级语言的层面来讲,引用与指针没有任何关系,引用就是一个变量的别名,对引用的操作等同于对被引用变量的操作
剩下的什么引用必须初始化且不能改变,局部变量不能返回引用,这种简单问题我相信学计算机的都知道,我就不说了==。

五,由栈对象引发一些问题
学过java的小伙伴都知道,java中没有栈对象这一回事,而在C++中,A a;就创建了一个栈对象,(A 为定义好的类),这个对象在创建时是要进行初始化的,而在java中这只定义了一个A类型的引用(java中引用实际是不能对地址进行操作的指针,跟C++中的引用还不一样,反正我是这么理解的==)下面看一段程序

#include<iostream.h>class innner_class{    private:        int x;    public:        inner_class(int z)        {            x = z;        }        void write()        {            cout<<x<<endl;        }};class outer_class{    private:        int y;        inner_class x;        inner_class r;    public:        outer_class(int z);        void write()        {            cout<<y<<endl;        }        void write_inner_x(){x.write();}        void write_inner_r(){r.write();}};outer_class::outer_class(int z):x(200),r(-300){    y = z;}void main(){    outer_class object(-12);    object.write_inner_x();    object.write_inner_r();    object.write();}

程序执行结果:
200
-300
-12
这个程序没有一点难度,注释我就不写了,我们主要关注的是他的执行过程,main函数中,当我们定义了一个对象后,这个对象里面的成员都是需要进行初始化的,(java里面根本不存在这个问题,因为对象只能够通过new在堆上产生) 对于基本数据成员来说,如果不进行显式的初始化,系统会自动为我们添加一个不确定的值,这是可以的,但对于对象成员来说,如果在初始化列表中没有显式的给出,那么系统就会调用默认构造函数来初始化对象成员,这时,如果没有默认构造函数那么就会报错。
注意:在构造函数体内的赋值等操作不能叫初始化
所以我们有以下结论:
必须使用成员初始化列表的有:
没有默认构造函数的对象成员
const成员
引用类型成员

还要注意的是初始化的顺序与列表的顺序无关,是在类中定义的顺序决定
如果把继承加进来,这个问题会稍微复杂一些,例子程序我就不写了,大同小异
**先说一下干货重要结论:
1.先调用基类的构造函数,按照继承操作时在冒号给出的基类顺序进行,如果基类中还有对象成员,则先调用对象成员的构造函数,在执行基类的构造体
2.调用对象数据成员的构造函数,按照他们在类中定义的顺序进行
3.执行本类的构造函数**
我从来都不喜欢背这种硬生生的结论,还是像前面所说,抓住一个本质问题:所有的对象成员都需要进行初始化
我们来看派生类继承了基类之后,也继承了他的所有成员,那么,所有的成员都要进行初始化,初始化基类中的成员,一定要调用他的构造函数,如果基类中有对象成员,这里说的调用基类对象的构造函数我的理解是通过其构造函数的初始化列表隐式或显式调用,所以只要理解到这里,这样的规则就很容易记住
六,复制构造函数
C++中的这个东西从刚开始就引起我的兴趣,我就感觉他跟java里面的那个clone() 函数怎么就那么像 哈哈
首先明确一个问题,一个类中的复制构造函数是必有的,如果没定义,系统会自动生成一个默认复制构造函数,对于这个函数简单的性质,什么浅复制,深复制我就不说了,说几个要注意的问题

class A{    int x;    int y;public:    A(int a,int b)    {        x = a;        y = b;    }    A(A &m)    {        x = m.x;        y = m.y;    }};void main(){    A a1(10,20);    A a2(a1);}

看上面这个程序,很简单,其中A a2(a1)是利用一个对象初始化另个对象。注意,如果复制构造函数的参数不是引用是对象,将无限循环出错。
**下面是调用复制构造函数的情况:
1.当一个函数的形参是对象时,将自动调用复制构造函数(形参是引用或指针,则不调用)
2.函数返回值是对象,自动调用**
其实,C++语言是利用复制构造函数在进行一种值传递,类似java的clone()h函数


当然C++对比java语言语法显得繁杂很多,但是由于指针的存在,在处理某些问题时显得灵活得多,代码有时也精简,我如果再遇到其他C++有趣的问题,会持续更新我的博客。

原创粉丝点击