规范 效率的C++

本文介绍了C++编程中的一些最佳实践,如使用头文件保护符避免重复定义,使用class前置声明减少编译次数,构造函数初始化列表提高效率,常量成员函数,值传递与引用转递的优劣,以及返回引用类型带来的灵活性。
摘要由CSDN通过智能技术生成

目录

简言

1.头文件保护符

2.C++头文件中class前置声明代替头文件

3.用构造函数初始化列表来初始化成员变量

4.常量成员函数        

5.值传递和引用转递

6.返回引用类型和返回值类型


简言

        在编写C++程序中,我们应该培养一些良好的编程习惯,以及有些可能会影响到程序性能的地方需要注意。

1.头文件保护符

        使用头文件保护符是为了避免重复定义的错误。

#pragma once

#ifndef TEST_H
#define TEST_H

class Test
{
public:	
	Test() {};
private:
	int b;
};

#endif 

        确保头文件多次包含仍能安全正常工作的是用预处理功能,这里的#define指令把一个名字设定为预处理变量,预处理变量有两种状态:已定义和未定义。#ifndef当且仅当变量未定义时为真,#ifdef当且仅当变量已定义时为真。一旦结果检查为真,则执行后续操作,直到遇到#endif指令为止。这样可以确保这个头文件被多次包含时,仍然只编译一次。在使用visual studio编译器时,vs会提供#pragma once指令,也是同样的效果。

2.C++头文件中class前置声明代替头文件

        C++编译是一个比较费时的事情,所以我们应该要减少编译时间,所以要更好地包含头文件。在一个头文件中,我们免不了会使用外来类,此时我们应该尽可能的用class来声明外来类,而不是直接包含外来类的头文件。

#include <iostream>
#include "Test.h"	//包含头文件
class Test;		//用class前置声明

class Main
{
private:
	Test p;
};

        这里,当我们有一个两个类,一个class Test,一个class Main。采用头文件将类的声明和实现分开,这样就会有四个文件:Test.h,test.cpp,Main.h,Main.cpp。如代码所示,Main中声明了一个Test成员变量,所以包含了test.h文件。 

        case1:当我们更改了test.h文件,比如删掉了一个变量,那么Main.h会不会重新编译呢?当然是会的,因为变量对象p的大小改变了,不仅仅是Main会重新编译,所有使用了Test类的对象的文件都要重新编译。

        case2:和case1同样的问题,不同的是我们用class前置声明代替#include “Test.h”呢?答案是确实不会重新编译,但是编译会报错,因为声明Test变量时,只用前置声明,编译器不知道Test有多大,需要为变量对象p分配多大的内存。

        case3:如果我们声明的是指针对象能不能解决case2的问题呢?答案是可以的,因为指针的内存大小是固定的,32位占四个字节,64位占8个字节。为了能够在Main.cpp文件中能够使用Test方法应该包含Test.h文件。

        case4:看到这里是不是还没有理解到使用前置声明的好处呢?我们结合case1和case2和case3,当我们在Main.h中使用前置声明,在Main.cpp中包含Test.h文件。当Test.h文件变化时,由于Main.cpp包含了Test.h文件,所以需要重新编译,而Main.h文件不需要重新编译,所以:当我们所有用到Main类的其他地方,他们所在的文件都不需要重新编译了。因为Main.h文件没有变,接口没有变,也没有新增减少变量。

        为什么我们说的是尽可能使用class前置声明,而不是所有的呢?因为有一些情况是不能用class前置声明的。以下三种情况是不可以用的(用Main和Test代表):1、Main类是继承自Test类;2、Main类中包含Test成员变量;3、Main中的inline函数中引用到了Test类的成员。这三种情况必须要包含头文件,其余时候我们尽量使用class声明来节省编译时间。

        总结:对一个c++类来说,如果它的头文件改变了,那么所有包含这个类的对象的文件都需要重新编译,而如果只是cpp文件改变了,但是头文件却没有改变,那么所有包含这个类的对象所在的文件都不会重新编译。

3.用构造函数初始化列表来初始化成员变量

        我们在写构造函数的时候,应该用初始化列表来初始化成员变量,而不是在构造函数内部进行赋值。

class Main
{
private:
	Main(int a,int b) :re(a), im(b) //初始化列表  
	{ }
	Main() { re = 0; im = 0; }	//对成员变量赋值
private:
	int re;
	int im;
};

        初始化和赋值有什么区别呢?简单来说就是,初始化是在编译器为对象分配内存时赋一个初始值,在这个对象的生命周期有且仅有一次,而赋值是清除掉这个对象之前的值,再赋值一个新值。所以:当我们使用初始化列表时是直接对成员变量初始化,而在构造函数里面赋值,成员变量已经被编译器默认初始化了,然后再赋值,效率就低了。

4.常量成员函数        

        不改变成员变量的函数应该定义成常量成员函数,该加的const一定要加。

class Complex
{
public:
	Complex(int a, int b) :re(a), im(b){}
	int getRe() { return re; }
	int getIm() const { return im; }
private:
	int re;
	int im;
};
int main()
{
	Complex p(1, 2);
	std::cout << p.getRe() << std::endl;
	const Complex d(2, 4);	//声明一个常量对象
	std::cout << d.getRe() << std::endl; //对象含有与成员 函数“Complex"getRe"不兼容的类型限定符对象类型是: const Complex
	std::cout << d.getIm() << std::endl;
}

        因为我们在定义类的对象时,有可能会定义成常量对象,而如果调用的函数不是常量成员函数,编译器就会报错。

5.值传递和引用转递

        能使用引用转递都应该使用引用传递。我们知道引用是对象的别名,编译器不会对引用分配内存空间,所以感觉引用是看不见摸不着的,但是在底层实现时,引用和指针的实现方式一样。

        第一个好处:当我们在使用值传递时,需要进行一次赋值拷贝,而使用引用姑且可以把它看成指针占四个四节,就可以减少这一次复制拷贝,尤其是自定义类型的时候,数据往往过大,那么赋值拷贝效率就会比引用降低很多。

        第二个好处:当我们使用引用传递时,可以直接修改引用对象的值,而如果不需要修改引用对象的值,只需要加上const关键字即可。

6.返回引用类型和返回值类型

        返回引用类型的好处:接受者无需知道是以引用的形式返回。为什么这么说呢?是因为引用可以左值。

class Complex
{
public:
	Complex(int a, int b) :re(a), im(b){}
	int& getRe() { return re; }
	int getIm() const { return im; }
private:
	int re;
	int im;
};
int main()
{
	int a = 10;
	Complex p(1, 2);
	a = p.getIm();		//函数返回值,做右值
	a = 20;
	p.getRe() = a;		//函数返回引用,可做左值

}

        当我们使用引用做返回类型时,提供的选择更多,可以作为左值。但是切记,不能返回局部变量的引用,因为局部变量的生命周期随着函数的执行结束就结束了,那么它的引用就是无效内容。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值