c++ const关键字详解

        正所谓酒足思淫欲,当衣食无忧的时候自然会产生很多的歪想法,就像当官一样,权力越大,越容易腐败。

        《Effective C++》中第三条:尽量使用const。这就说明使用const是有很大的好处的,const就是把权力尽可能控制一下,这样就会减免很多出bug的机会。因为一个项目不可能只有一个人开发,即使是一个人开发,当涉及到文件多的时候,就会容易出现差错,如果在另外一个文件中修改了一个本不应该修改的变量,就会出现很大的错误。

        const就是将修饰的部分确定为常,所谓常就是不让修改,比如常量,常对象,常指针等等。当我们试图修改这些常量的时候,编译器就会报错,就会阻止错误的发生。下面来看看有哪些const用法。

一、常量

        声明常量的形式:const 数据类型 数据名;

        常量就是不允许修改其值,定义的时候必须赋初始值,不允许再次赋值,这个很好理解。那一个类中也可能会有常数据成员,而我们在类体中往往声明为const,初始化则是在构造函数中,这个时候构造函数的写法就有规定了,看下面的程序:

class Test
{
	public:
		Test(int x, float y):a(x), b(y){} // 如果是Test(int x, int y){a = x; b = y;},编译就会出错
	private:
		const int a; // 声明常数据成员
		float b;
};

int main()
{
	const int t = 10; // 定义常变量(既然是常的怎么又是变量呢哈哈)
	t = 20; // 错误,常量不能再次赋
	Test test(1, 1.1);
}

看着这个构造函数有点别扭,这才是初始化,而后面那种错误的写法就是我们经常写的构造函数,这个只是赋值,但是我们的常数据成员是不能给赋值操作的。所以只能写成第一种写法,而且强烈推荐将构造函数写成这种形式。在main函数里面定义了一个常变量t,定义的时候必须赋初始值,以后都不能再对t进行赋值操作了,否则编译会出错。这里还有个声明和定义的区别,这也是一个比较难理解的概念,类体里面是声明了一个常数据成员,而main函数里则是定义了一个常变量,查查资料好好体会一下。

二、常对象

        声明常对象的两种形式:const 类名 对象名(参数列表);

                                               类名 const 对象名(参数列表);

        什么是常对象呢,就是对象不允许改变,也就是说,不能修改常对象的任何成员变量,当一个对象定义为常对象的时候,任何修改其成员变量的操作都会报编译错误的。看看下面这个程序:

#include<iostream>
using namespace std;

class Test{
	public:
		Test(int x, float y):a(x), b(y){} // 推荐这种写法
		void print()
		{
			x = 10; // 对x进行赋值
			cout << "x:" << x << ", y:" << y << endl;
		}
	private:
		int a;
		float b;
};

int main()
{
	Test test1(1, 1.1);
	test1.print();
	const Test test2(2, 2.2);
	test2.print(); // 编译出错,因为print()方法中有修改test2的成员变量
}

有人说,我自己写的程序干嘛没事在print()函数里面写个修改x值的代码啊,吃饱了撑着。说的对,正常情况下不会这样,但这也只是你一厢情愿罢了,你的程序就允许这样写,而且这样写这一行是完全没问题的,这就为出错埋下了伏笔。但有时候确实需要这样写,比如我就要每次输出x都为10,这就要看自己程序的必要性了。所以我们将这一行删掉,不改变常对象的成员变量就OK了,所以我们定义对象的时候,如果不需要修改它的成员变量的时候就定义为常对象。这个问题是解决了,但是我们改了再编译一次,还是会报错,那就是常对象只能调用常成员函数。

三、常成员函数

        声明常成员函数的形式:返回值类型 成员函数名(形参列表) const;

        上面提到的,当test2调用print()函数的时候就会报错,这是因为test2是常对象,而print()函数是非常成员函数,所以不能调用。为啥嘞,定义成常对象,当然不想对象的成员变量被修改,非常成员函数是允许修改该类的成员变量的,所以二者冲突了。看下面这个程序:

#include<iostream>
using namespace std;

class Test
{
	public:
		Test(int x, float y):a(x), b(y){}
		void print() const
		{
			x = 10; // 编译出错
			cout << "x:" << x << ", y:" << y << endl;
		}
		void setA(int x)
		{
			a = x;
		}
	private:
		int a;
		float b;
};

int main()
{
	const Test test1(1, 1.1);
	test1.print();
	test1.setA(10); // 编译错误,常对象不能调用非常成员函数
	Test test2(2, 2.2);
	test2.print(); // 非常对象可以调用常成员函数
	test2.setA(10);
}

上面程序中,x=10这行编译就会出错,因为函数为const型,所以是不允许修改类的成员变量的,所以当我们的函数没有涉及到成员变量的赋值操作,将其声明为const型是不是更安全呢。test2是非常对象,即可以调用非常成员函数,也可以调用常成员函数,这个很好理解的。

四、指向对象的常指针

        声明常指针的形式:类名 * const 指针变量名;

        指向对象的常指针,指针是常,就说明指针变量是不能改变的,在定义一个常指针的时候就必须要赋初始值,以后再不能对其修改,也就是说这个指针再不能指向其他的对象了,但是指向的这个对象是可以改变属性值的。看下面这个程序:

#include<iostream>
using namespace std;

class Test
{
	public:
		Test(int x, float y):a(x), b(y){}
		void print()
		{
			cout << "a:" << a << ", b:" << b << endl;
		}
		void setA(int x)
		{
			a = x;
		}
	private:
		int a;
		float b;
};

int main()
{
	Test test1(1, 1.1);
	Test * const p = &test1;
	p->print();
	p->setA(10); // 这里修改对象test1的属性值
	p->print();
	Test test2(2, 2.2);
	p = &test2; // 编译错误,p为常指针,不能再赋值
}

调用setA()方法对对象test1的属性值进行修改是可以的,因为test1不是常对象,它的属性值是可变的,除非该属性声明为const。但是在p = &test2的时候,就会报编译错误,因为p已经定义是常,所以它的值是不能修改的,也就是说不能再指向其他的对象。

五、指向常对象的指针

        声明指向常对象的指针的形式:const 类名 * 指针名;

        指向常对象的指针,这里相当于在上层将接受点缩小,就是说不允许通过指针来修改指向对象的属性值,这样可能有点难以理解,结合下面的例子再来说吧:

#include<iostream>
using namespace std;

class Test
{
	public:
		Test(int x, float y):a(x), b(y){}
		void print()
		{
			cout << "a:" << a << ", b:" << b << endl;
		}
		void setA(int x)
		{
			a = x;
		}
	private:
		int a;
		float b;
};

int main()
{
	const Test test1(1, 1.1);
	Test test2(2, 2.2);
	const Test * p1 = &test1;
	p1->setA(10); // 编译错误,指向常对象的指针是不允许通过指针修改指向的对象的属性
	p1 = &test2;
	test2.setA(10); // 没问题
	p1->setA(10); // 编译错误,指向常对象的指针是不允许通过指针修改指向的对象的属性
	Test * p2 = &test1; // 编译错误,常对象只能用指向常对象的指针来指向它
	const Test * const p3 = &test1; // 指向常对象的常指针
	p3 = &test2; // 编译错误,常指针不允许再赋值
	p3->setA(10); // 编译错误,不允许通过指向常对象的指针来修改指向对象的属性值
}

看了程序里面的注释是不是感觉更加郁闷了,又是对象又是常对象又是指针又是指向常对象的指针又是指向常对象的常指针,阿弥陀佛。这样来理解吧,其实我们定义一个指针的目的,当然是通过指针来操纵指向对象,那么如果我们的对象没做什么特殊的限制,那么我们想要保护该对象就只能从操纵者指针这里来设置一些限制。就相当于一个瓶子,我们不想让瓶子里面放大的东西,但是我们又想让瓶子大一点好容纳更多的东西,这样的话我们就把瓶口做小一点,不让大的东西进来。这里也是一样,如果我们定义一个一般的对象test2,那么我们选择性的给他设置瓶颈,也就是指针,既可以用一般指针指向它,也可以用指向常对象的指针p1指向它,如果是p1的话,那么我们就不能通过p1来修改它的属性值,这样是不是就达到了瓶颈的目的呢。但是对于一个常对象,本来瓶子就小,当然不能有更大的瓶颈,所以只能用指向常对象的指针来指向它。最后又有个指向常对象的常指针,这是一个最特别的指针,我们既不能修改指针的值又不能通过指针来修改对象的属性值,这个就是最小的瓶颈,这些东西呢,我们都要根据实际情况来用,根据实际情况来设置我们的瓶颈,让哪些大小的东西进来,不让哪些大小的东西进来。

六、常形参

        在定义函数的时候,常形参有两种形式,一种是指向常变量的指针,一种是常引用,其实二者的作用都是一样的,都是防止在函数体中改变实参变量的值。看下面这个程序吧:

void swap1(int a, int b)
{
	int temp = a;
	a = b;
	b = a;
}

void swap2(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

void swap3(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

void print(int a, int b)
{
	cout << "a:" << a << ", b:" << b << endl;
}

int main()
{
	int x1 = 10, x2 = 10, x3 = 10, y1 = 20, y2 = 20, y3 = 20;
	swap1(x1, y1);
	print(x1, y1);
	swap2(&x2, &y2);
	print(x2, y2);
	swap3(x3, y3);
	print(x3, y3);
}

这三个方法的目的都是用来交换a和b的位置,但是具体能不能成功就要看程序运行的结果了,运行完程序后,发现x2和y2,x3和y3的位置是交换了,但是x1和y1却没有,这个我们都了解过参数的值传递和引用传递吧,这个不用多说,在swap1方法中只是传递了一个复制的值,实参当然不会交换。swap2形参是指针,这也是值传递,只不过传递的是实参的内存地址,交换的还是实际参数。swap3传递的是引用,也就是实际参数,所以也会交换成功。我们看出,在swap2和swap3方法里面都对实际参数进行了修改,如果我们不想修改传递的实际参数怎么办呢,只要在形参前面加一个const来修饰,这样的话在函数体里面是不能修改参数的值的,这样就起了一个保护的作用,具体哪些地方需要保护就根据自己的程序决定,当前这个当然不用保护,不然我们怎么达到交换位置的目的呢。

        大概我们经常接触到的const用法就这些,尽量多用const可以让我们的程序更加的安全,尽量的限制了bug的产生,细节决定成败。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值