C++QT设计模式(第二版)

《C++QT设计模式(第二版)》读书笔记

Alan Ezust

Paul Ezust

好习惯:

定义,应将它的定义放到一个与这个类同名的头文件中,并添加预处理宏来避免多次包含。例:aclass.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _ACLASS_H_
#define _ACLASS_H_
#include<Qstring>
class Aclass{
public:
...
};
#endif

再将这个类成员函数的具体实现写入一个单独的实现文件中去,命名为同名的cpp文件:

1
2
3
4
5
6
#include<QString>
#include"aclass.h"
QString Aclass::aprintfunc() const{
...;
}

默认情况下,类的成员是private类型

类的友元

友元可以是一个类,另外一个类的成员函数,或者任何一个非成员函数。

破坏封装性可能会危害程序的课维护性,因此应该尽量少的使用友元,不得已使用时候也要倍加小心。

通常而言,为了达到以下两个目的才是用友元函数:

  • 为了使用工厂方法,此时需要对某个类强制实施某些创建规则。(具体待补充)
  • 为了使用全局运算符函数,比如operator<<()

析构函数

如果某个类满足以下条件,则无需提供析构函数:

  • 只拥有非指针的简单类型成员
  • 拥有已经适当定义的析构函数的类成员
  • 它是满足某些条件的一种QT类(待补充)

stactic

全局命名空间污染:将名称添加到全局作用域中(例如,声明全局变量或者全局函数)

这是不好的编程风格

相对于全局变量,我们更倾向于使用static成员(且一般可以代替全局变量),因为static成员不会在全局命名空间中增加不必要的名称

每个static数据成员都必须在类定义之外的文件中被初始化(定义),一般在对应的类的实现文件中初始化。

.cpp中

int Thing::s_count = 0;

类的声明和定义

例:

egg.h:

1
2
3
4
5
6
7
[...]
#include"chinken.h"
class Egg{
public:
Chicken* getParent();
};
[...]

chicken.h:

1
2
3
4
5
#include "egg.h"
class Chicken{
public:
Egg* layEgg();
};

一个头文件包含另外一个头文件,另一个又包含回来了,预处理器不允许这样的循环依赖。

这两个头文件都不需要包含另一个头文件,因为这样做会不必要的在头文件之间建立一种强依赖性

在正确的情况下,C++允许使用前置类声明,而不必包含某个特定的头文件。

例:egg.h

1
2
3
4
class Chinken;//前置类声明
class Egg{
Chinken* getparent();
};

注意:cpp文件中可以包含多个头文件,而不会导致它们之间的循环依赖性。.cpp文件对两个头文件具有依赖性,但头文件之间彼此不存在依赖性。

例:egg.cpp

1
2
3
4
5
#include"chicken.h"
#include"egg.h"
Chicken* Egg::getParent(){
return new Chicken();
}

因此,前置类声明使得定义双向关系成为可能,

转换构造函数

当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。

用转换构造函数可以将一个指定类型的数据转换为类的对象,但是不能反过来将一个类的对象转换为一个其他类型的数据。 类型转换函数的作用是将一个类的对象转换成另一个类型的数据。

转换构造函数的作用是将某种类型的数据转换为类的对象,当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。试验了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A
{
public:
int a;
A(int a) :a(a) {}
A reta()
{
return a;
}
};
int main()
{
A a(2);
A b = a.reta();
A c = 3;
cout<<b.a<<"\n"<<c.a<<endl;
return 0;
}

结果是输出2和3

实际上这是由隐式转换机制造成的,如果不想要这种效果,可以在构造函数前加上explicit声明。加上之后上面的代码就会编译出错,提示

无法从“int”转换为“A”。

既然能将数据转换为类型,类型也能转换为数据。c++的类型转换函数可以将一个类的对象转换为一个指定类型的数据。

类型转换函数的一般形式为 :

operator 类型名()

{实现转换的语句}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A
{
public:
int a;
A(int a) :a(a) {}
operator int()
{
return a;
}
};
int main()
{
A a(2);
int b = a + 3;
A c = a + 4;
cout<<b<<"\n"<<c.a<<endl;
return 0;
}

结果输出5和6

const成员函数

当将关键字const应用到一个(非static的)类成员函数时,其含义是比较特殊的。如果放置在参数之后,const就成为函数签名的一部分,从而保证此函数不会改变主题对象的状态。

要理解只一点,好方法就是意识到每一个非static成员函数都有一个称为this的隐式参数,this是一个指向主题对象的指针。当将一个成员函数声明为const时,就相当于告诉编译器只要此函数被调用,this就是一个指向const的指针。

可以看一下原始的C++到C语言的预处理器是如何处理成员函数的。因为C语言不支持重载函数或成员函数,预处理器会将这些函数翻译成具有“重整名称”的C语言函数,例:

1
2
3
4
5
6
cpp中
class Point{
public:
void set(int nx ,int ny){}
Qstring toString() const{}
};
1
2
3
c语言预处理后大概版本:
_Point_set_int_int(Point* this,int nx,int ny)
_Point_toString_string_const(const Point* this)

用QT实现Unicode显示

QtextStream能够针对Unicode的QString和其它类型的Qt起作用

所以配合QString的QtextStream可以顺利实现打印中文字符

1
2
3
4
5
6
7
8
9
10
11
#include<QTextStream>
#include<QString>
int main()
{
const char* charstr="这是中文";
QTextStream cout(stdout,QIODevice::WriteOnly);//WriteOnly,可省略:QTextStream cout(stdout);
QString str=charstr;
cout<<str<<endl;
return 0;
}

C++中避免使用数组

在C++中,数组被看成是“邪恶的”,

下面是应该避免在C++中使用数组的一些理由:

  • 编译器和运行时系统都不会检查数组的下标是否位于正确范围之内
  • 使用数组的程序员有责任编写额外的范围检查代码
  • 数组的大小可以是固定不变的,或者必须使用堆中的动态内存
  • 对于堆数组,程序员有责任确保所有情况下正确释放内存
  • 为此需要深入理解C++已经异常,特别是发生异常时的底层处理机制。向数组插入、预分配或追加元素都是费时的操作(再运行时和开发时都是如此)。

QStringList与迭代

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<QStringList>
#include<QDebug>
int main()
{
QString winter="December,January,February";
QString spring="March,April,May";
QString summer="June,July,Auguest";
QString fall="September,October,November";
QStringList list;
list<<winter;
list+=spring;
list.append(summer);
list<<fall;
qDebug()<<"The spring mouths are: "<<list[1];
QString allmonths=list.join(", ");//把元素连成一个字符串
qDebug()<<allmonths;
QStringList list2=allmonths.split(", ");//按某个字符,把一个字符串拆成列表
qDebug()<<list2.size();
qDebug()<<list2[1];
foreach(const QString &str,list){//QT foreach循环——类似于python的for循环
qDebug()<<QString(" [%1] ").arg(str);
}
//C++STL风格的迭代
for(QStringList::iterator it =list.begin();it!=list.end();++it){
QString current =*it;
qDebug()<<"[["<<current<<"]]";
}
return 0;
}

运算符重载

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Complex{
friend ostream& operator<<(ostream& out,const Complex& c);
friend Complex operator-(const Complex& c1,const Complex & c2);
friend Complex operator*(const Complex& c1,const Complex & c2);
friend Complex operator/(const Complex& c1,const Complex & c2);
public:
Complex& operator+=(const Complex & c);
Complex& operator+=(const Complex & c);
Complex operator+(const Complex & c);//这里应该将运算符重载函数声明成非成员友元函数
private:
double m_r,m_i
}

例子中声明的运算符都是二元的(接收两个操作数)。对成员函数而言,却只有一个形式参数,因为第一个(左边的)操作数都是隐式的:*this

虽然可以将运算符重载为成员函数或者全局函数,但需要注意,它们的主要差异是调用它们的方式。特别地,成员函数运算符要求有一个用作左操作数的对象,而全局函数允许对任何一个操作数进行某种类型转换。

例:

1
2
3
4
5
6
7
8
9
10
//为什么+更适合作为非成员函数的理由
int main(){
Complex c1(4.5,1.2);
Complex c2(3.6,1.7);
Complex c3=c1+c2;
Complex c4=c3+1.4;//提升右操作数
Complex c5=8.0-c4;//提升左操作数
Complex c6=1.2+c4;//错误,左操作数没有针对成员运算符而提升
}

所以重载操作符应该视情况重载全局的运算符,将全局运算符重载函数声明成非成员友元函数

从函数返回应用

有时候将函数设计成返回引用是非常有用的。例如,可以将多个操作“链”起来:

cout<<thing1<<thing2<<thing3...;

返回引用(尤其是*this)通常用来给成员函数提供左值行为。

采用引用参数,就可以通过将对象的别名指定成const来保护返回的引用。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int& maxi(int& x,int& y){
return(x>y)?x:y;
}
int main(){
int a= 10,b=20;
maxi(a,b)=5;//给b赋值5
maxi(a,b)+=6;//给a增加6,a现在为16
++maxi(a,b);//给a增加1
cout<<a<<"\t"<<b<<endl;
return 0;
}

注意:要小心,不要让函数返回一个临时(局部)对象的引用。当函数返回时,所有局部变量都销毁了。

inline函数的一些规则

  • inline函数必须在被调用之前定义(仅仅声明它是不够的)。
  • 在一个源代码模块中只能有一次inline定义。
  • 如果类成员函数的定义出现在类定义之内,则成员函数就是隐含inline的。

如果函数太复杂,或者编译器选项改变了,则编译器可能会忽略inline指令。大多数编译器会拒绝包含如下语句的inline函数:

  • while,for,do-while语句
  • switch语句
  • 超过一定数量的代码行

继承和多态

没有virtual时候*p1和*p2的具体类型靠函数指针类型来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
class A
{
public:
void getnum() {//A类中没有virtual函数
std::cout << a << std::endl;
}
private:
int a;
};
class B:public A
{
public:
void getnum() {
std::cout << b<< std::endl;
}
private:
int b;
};
int main() {
A* p1 = new(A);
A* p2(new B);
std::cout << typeid(p1).name() << std::endl;//打印A*,指针类型始终不变
std::cout << typeid(p2).name() << std::endl;//打印A*,指针类型始终不变
std::cout << typeid(*p1).name() << std::endl;//打印A
std::cout << typeid(*p2).name() << std::endl;//打印A
std::cin.get();
return 0;
}

virtual时候*p1和*p2的具体类型通过指向类型判断,虚函数有虚函数表,可以确定数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
class A
{
public:
virtual void getnum() {
std::cout << a << std::endl;
}
private:
int a;
};
class B:public A
{
public:
void getnum() {
std::cout << b<< std::endl;
}
private:
int b;
};
int main() {
A* p1 = new(A);
A* p2(new B);
std::cout << typeid(p1).name() << std::endl;//打印A*,指针类型始终不变
std::cout << typeid(p2).name() << std::endl;//打印A*,指针类型始终不变
std::cout << typeid(*p1).name() << std::endl;//打印A
std::cout << typeid(*p2).name() << std::endl;//打印B,根据虚函数表判断具体指向数据类型
std::cin.get();
return 0;
}
坚持原创技术分享,您的支持将鼓励我继续创作!