目录
简言
在编写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; //函数返回引用,可做左值
}
当我们使用引用做返回类型时,提供的选择更多,可以作为左值。但是切记,不能返回局部变量的引用,因为局部变量的生命周期随着函数的执行结束就结束了,那么它的引用就是无效内容。