C++为一个语言联邦
1.概念:C++是一个面向对象的程序程序设计,那么对于面向对象,他是C语言的衍生版,所以它既可以进行C语言的过程化设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
2.对于C++而言,它之所以被称之为语言联邦,是因为它有四个次语言, 如下:
- C语言:由于C++语言是C语言的衍生出来的,以C语言为基础,所以具有C语言的一些行为,如:区块,语句,预处理,内置数据类型,数组,指针等都是来自C语言的。
- 面向对象的C++:有类的操作,对于类成员,包含有构造函数和析构函数等;对于类的操作,有类的继承,多态,虚函数等。
- 模板:在C++中的泛型编程部分。
- STL:是一个程序库,里面包含了许多非常实用的东西,例如:容器,迭代器,算法,函数对象,并且里面的源码是非常的经典。
尽量用const,enum,inline代替#define
为什么说要用他们去替换掉#define呢? 1.首先,对于#define定义的一些常量,例如:#define ASPECT_RATIO 1.635 定义了一个常量,在程序运行的时候,那么在进行预编译的时候,会直接将程序中的ASPECT_RATIO这个定义的常量直接用1.632替换掉了,并且这个常量的名称有可能连记号表的进不去,这样做是很可怕的,例如:当你用此变量得到一个编译时错误的信息时,他会提示的是1.632出现错误,如果写的很复杂的话,那么完全不知道错误出现在哪,连追踪都无法追踪。 而解决的时候可以通过const去解决,例如:
const double AspectRatio = 1.635;
这样作为一个语言常量,AspectRatio肯定会被编译器看到,并且也会进入记号表中,如果其出现问题的时候,可以直接定位。
2.其次,对于用const替换#define一般情况下有两个情况:
①: 定义常量指针: 对于定义常量指针,我们一般会通过string来定义,因为如果用char* 来定义的话对于const的定义会出现分期:
const char* authorName = "abc"
char* const authorName = "abc"
const char* const authorName = "abc"
而如果使用的是string的话,直接写一次const即可,如下:
const std::string authorName("abc")
②:class的专属常量,这个常量的作用域仅限制于这个类内。 如下进行定义:
class GamePlayer
{
private:
static const int NumTurns = 5;
int scores[NumTurns];
};
因为其NumTurns是类成员中的static成员,所以他可以是声明式,对于他,可以在类中去使用,但是只要不去取它们的地址,就可以直接用声明式,不需要在给其定义以下,如果要取其地址,那么要在类外给其一个这样的定义:
const int GamePlayer::NumTurns;
对于的static成员变量,以及成员函数,它的作用域会延长。所以对于类中的其成员和函数,这些函数及成员变量只能访问静态的变量以及函数,不能访问非静态的成员和函数,而非静态的成员和函数可以访问其。并且在继承中,static的一系列成员都只有一份,不管如何继承,他们就只存在一份。
③:对于#define,他不能定义类的常量成员,因为对于#define,它不重视作用域,一旦宏被定义,那么在其后的编译过程中就有效,所以它不仅不能用来定义class的专属常量,而且也不能提供任何封装性,但是const成员可以。
3.对于2中还有一些问题:
对于一些类中的静态成员变量无法在类中被初始化的时候,那么对于二中的类的数组成员就无法创建,所以对于这种情况,外面一般是通过enum枚举类型的数值进行使用,如下:
class GamePlayer
{
private:
enum{ NumTurns = 5};
int scores[NumTurns];
};
对于enum的使用,要注意以下情况: ①:对于enum,他是没有地址的,取它的地址是违法的。(对于const 取其地址是可以的,而对于enum和#define定义的常量,取其地址是违法的) ②:实用。
4.将函数声明为inline内敛函数: ①:对于这个函数,他会将这个函数在被调用的地方被展开,就降低了调用函数这一部分带来的时间的浪费。 但是对于#define ,会出现一些错误,例如:
#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))
对于上面这个宏定义,我们可以看出他是想取两个数的最大值的但是,对于以下不同的输入,它会有不同的答案:
int a = 5,b = 0;
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);
而对于这样的错误,我们完全可以使用inlne函数取解决,因为我们对于其的需求都是一样的:
template<class T>
inline void CALL_WITH_MAX(const T& a,const T& b)
{
f(a > b ? a : b);
}
此时我们调用其函数就不怕出现参数被多次求,因为传进来的参数就是已经被加过的,它会遵守作用域的规则。
尽量使用const
对于const,我们在上面也有所涉及,它是将一个数据或者函数定义为其提供一个常属性,这样可以让编译器帮助我们去看管这个不变的量,如果它出现改变,编译器就会首先不同意,来阻止这样的行为。
const用法很多,它不仅可以修饰类外的全局常量,也可也修饰类内的静态成员和非静态成员,还可以修饰函数。
1.对于const修饰指针的时候,const放在*号的左右位置不同,对应的指针和指针的解引用的结果是否可以改变是不同的,如:
const char* authorName = "abc"
char* const authorName = "abc"
const char* const authorName = "abc"
我们来重看上面这个代码,现在const的作用就体现出来了。
2.对于const修饰的函数可以让函数出现的错误被编译器识别出来,从而降低我们做的一些不必要的错误,例如:
class Rational{}
const Rational operator*(const Rational& lhs,const Rational& rhs);
上面这个重载*运算的函数为什么要用const进行修饰呢,主要也是为了防止我们出现以下错误,因为对于这个重载函数,我们肯定会对最终结果不会进行其他的赋值操作,所以我们在进行如下:
Rational a,b,c;
a*b = c;
这个操作的时候,就会出问题,给a*b的结果又赋予了一个c,这是一个很奇怪的做法,我们完全可以直接重新定义一个类对象,用c赋值,不必要进行如此麻烦的事情,所以对我们来说,这个代码就有一点的不尽人意,更甚至是在对此比较的时候,例如:
if(a*b = c)
其实我们是想说的是if(a*b == c) 这样的情况的,但是因为少写了一个等号,所以也有问题了,这个问题一般还无法通过编译器错误提醒得到,得步步调试找到问题,所以我们如果在函数的返回值前面加上const岂不是很好,直接让他的返回值无法进行改变,如果再次遇到前面这种情况的时候,那么就直接定位错误位置,岂不美哉。
const成员函数
1.对于类成员的函数,进行const的修饰会得到以下的情况:
- ①:它会使const接口比较容易理解。可以让我们知道哪个函数可以修改对象的内容,而哪个函数不可以。
- ②:它会使“操作const对象”成为可能。
为什么呢?因为对于C++而言,有重载函数,而且对于同一个函数,使用const修饰这个函数和不使用const修饰是两个函数,它俩是构成重载的,所以,例如在底层,他会对同一个函数进行不同的两种写法,分别有一个可以用const修饰,一个没有,我们可以根据自己的需求对其进行调用,例如:
class TextBlock
{
public:
const char& operator[](std::size_t position)const
{
return text[position];
}
char& operator[](std::size_t position)
{
return text[position];
}
private:
std::string text;
};
对于上面这个类成员对象进行操作的时候,如果我们实例化出类对象,而使用的是const成员对应的重载[]的函数进行操作,那么结果是不可以进行修改的,而使用非const成员的结果可以修改。(其中,使用const成员函数的时候,我们实例化出来的类对象,也应该是const修饰的,因为上面函数返回类型是引用,所以返回出来的值就是类中私有成员的text对应位置的字符)
2.bitwist const派和logical const派
- bitwist const派认为:对于一个const修饰的成员函数,这个函数不能修改对象内的任意一个bit位。
- logical const派认为:对于一个const修饰的成员函数,它可以修改对象内的bit位,但是必须是当数据量大的时候,客户察觉不到的时候进行修改。
①:但是对于bitwist const派的一些成员函数会出现以下这些缺陷,先看以下代码:
class CTextBlock
{
public:
char& operator[](std::size_t position)const
{
return pText[position];
}
private:
char* pText;
};
对于上面这个const声明,只是对这个类成员函数的this指针进行了const,所以说,在这个函数内,只要不改变类成员的数据就可以进行。 但是这个函数有个致命的弱点,就是如果我不在类成员函数中去改变pText,而在类外去改变,那么就会出现被修改,这显然就不符合bitwiste const的初衷了,例如:
const CTextBlock cctb("Hello");
char* PC = &cctb[0];
*PC = 'J';
如果进行上面的这个程序,那么pText还是被改变了。 从而也就出现了logical const派了。
②:logical const 派:由于他们认为,在数据高速缓存的时候,在用户无法知道的情况下修改bit位是可以容忍的,那么这就肯定会出现在const函数内去修改数据,那么就一定会被bitwise const派所不认同,这个时候我们只需要给在const函数内会被修改的数据在定义的时候加上关键词mutable即可,例如:
class CTextBlock
{
public:
size_t length()const;
private:
char* pText;
mutable size_t textLength;
mutable bool lengthIsValid;
};
size_t CTextBlock::length()const
{
if(!lengthIsValid)
{
textLength = strlen(pText);
lengthIsValid = true;
}
}
3.对于相同作用的const和non-const成员函数的代码重复问题处理
面对许多的代码重复,可能会伴随的一系列的问题,例如:编译时间,维护,代码膨胀等等,我们写代码肯定想着写的代码越少,且也能完成同样的任务,所以对于类成员中的const和non-const成员函数的重复代码处理是个很重要的问题。
①:对于相同的作用的const和non-const成员函数,其实他们主要的不同点就是在于是否会修改类成员的数据,如果不修改,我们就使用const函数,如果修改,我们就使用non-const函数,这样会让机器帮我们去保护数据的安全,而他们大部分其他的行为都是相同的,例如边界检查了,检查数据的完整性了等等。这些代码的重复显得有点让人很不舒服了,所以我们可以通过以下情况去解决:
class TextBlock
{
public:
const char& operator[](size_t position)const
{
return text[position];
}
char& operator[](size_t position)
{
return const_cast<char&>(static_cast<const TextBlock>(*this)[position]);
}
};
对于上面的函数,const函数是正常的,去处理const函数该处理的事情,而non-const函数的操作却不一样,他会将传进来的this指针进行一个强制类型转换位const类型,然后调用const类型的同类型函数,然后在返回的时候,会对返回值进行一次解const的操作,其中:
static_cast<const TextBlock>(*this)
const_cast<char&>
这样一来一回的操作,让这个函数的代码量减少了很多,而对于为什么只在non-const函数中去改变呢? 肯定是因为在non-const成员函数中,我们可以去随便修改数据(当然是可以修改),不会被限制,因为我们调用non-const函数的时候就想着数据会被修改,而const函数不可,他是常化的,我们修改的时候,编译器会首先不同意的。
确保对象在使用前已经被初始化
这个情况我们应该见的是比较多的,因为对于我们定义的一个数据,如果说这个数据没有被初始化,然后我们就去使用这个数据,对于一些语境,他会给这些未初始化的数据赋值为0;但是一般情况下,因为我们对这个数据没有进行初始化,但是对于另外一些数据,他会给这个数据赋予随机值,如果说,我们进行的数据量很大,而出现这种错误,他也不会报错,那么就会让我们陷入一个很尴尬的处境,所以说,我们在使用数据的时候,也要确保他被初始化后在进行处理。
1.对于类成员的初始化: 类成员的初始化一般会吧压力给到类的构造函数身上,但是对于构造函数的不同写法,会出现是对类成员进行初始化,还是进行赋值,且看以下代码:
class PhoneNumber{};
class ABEntry
{
public:
ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones)
{
theName = name;
theAddress = address;
thePhone = phones;
numTimeConsulted = 0;
}
private:
string theName;
string theAddress;
list<PhoneNumber> thePhone;
int numTimeConsulted;
}
对于上述这个类的构造函数,其实他并不是真正的给类成员进行了初始化,对于theName,theAddress,thePhone其实这是一个赋值操作,而不是初始化操作,因为,对于一个类来说,他的外置对象的类成员进行初始化的时候,初始化会放在进入构造函数之前,会发生在这些成员的default构造函数(一个可以被调用而不带任何实参者,它要么没有参数,要么参数都是缺省的)被自动调用的时候(也就是进入构造函数的内容之前)。而对于内置类型数据,就是上述类中的numTimeComsulted这个成员,对于它,它可以在定义的时候直接给值,所以他不保证在你所看到的哪个赋值动作之前就获得初值。
所以,对于上面所说的,要对外置对象进行初始化,那么我们首先在其进入类构造函数的本体的时候,我们调用以下其的default构造函数就可以了,如下:
class PhoneNumber{};
class ABEntry
{
public:
ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones):theName(name),
theAddress(address),
thePhone(phones),
numTimeConsulted(0)
{}
private:
string theName;
string theAddress;
list<PhoneNumber> thePhone;
int numTimeConsulted;
}
这样就可以了,但是值得注意的一点就是,在构造函数对数据进行初始化的时候,初始化的顺序是在类中定义时候的顺序,与构造函数后面的顺序无关。 而为什么我们要坚持使用初始化呢? 因为对于一个外置对象来说,我们定义了它,肯定会调用它的构造函数,那么我们肯定想在调用构造函数的时候,直接给定好它的值,而不是先构造函数之后,再进行赋值语句的拷贝函数,那么这样效率不就下降了。 但是对于内置类型,他们的构造和赋值的消耗没有多大差距,但是为了方便我们操作,我们一般也都在构造函数的时,直接在初值列(就是上述列类的构造函数名" : "后的内容)对其进行初始化。
2.不同单元的两个或者多个类进行操作的时候的初始化问题:
意思就是,当我们在一个类中去使用另一个类的实例化的对象去操作的时候,如果这个时候我们在类中使用的哪个类还没有被实例化出来,那么就悲剧了,我们使用一个未初始化的类对象去操作,那么这肯定会出错误。
而对于这种问题,我们只需要一个设计模式就可以解决了-----单例设计模式, 这个设计模式就是一个类只能实例化出一个对象,并提供一个访问它的全局访问点,这个实例并且被所有程序模块共享。但是这个实例化的对象要被static去修饰,因为,static对象,它的寿命会在被构造出来直到程序运行结束的时候停止,并且C++保证,函数内的本地静态对象会在这个函数被调用期间,第一次遇到该对象的定义式的时候被初始化,所以用函数调用(返回一个引用的方式)去替换直接访问非本地的静态对象(就是不是我这个单元的对象),所获得哪个引用就是经历了初始化的对象,并且,如果没有调用那个非本地的静态对象的话,那么也不用去承担其的构造和析构函数。 例如:
class FileSystem{};
FileSyetem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{};
Directory::Directory(params)
{
size_t disks = tfs().numDisks();
}
Directory& tempDir()
{
static Directory td;
return td;
}
这样,我们对这两个类进行操作的时候,我们直接用tfs()和tempDir()这两个函数就可以。
|