leetcode-整数翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func reverse(x int) int {
const (
intMax = 2147483647
intMin = -2147483648
)
result := 0
for x != 0 {
t := x % 10
if result > intMax/10 || (result==intMax/10 && t>intMax%10) { return 0 }
if result < intMin/10 || (result==intMin/10 && t<intMin%10) { return 0 }
result = result*10 + t
x /= 10
}
return result
}

刚开始使用了一开始就计算出最高位权值,进而直接乘上到达目的位的方法。
此种方法首先要事先求出数字位数才能求各位权值,进行一次循环浪费了时间,
求权值过程中又要进行一次循环,增加时间又增加空间,后续也没有太好的方法判断溢出。
类似

1
2
3
4
5
6
7
8
9
10
11
12
numLen := getNumLen(x)
w:=1
for i:=0; i<numLen;i++ {
w*=10
}
r := 0
for x!=0 {
t := x % 10
r += w * t

w /= 10; x /= 10
}

后来看了循环乘10只需要一次循环,还省去了许多过程与变量,又高效又优雅

1
2
3
4
5
6
7
r := 0
for x!=0 {
t := x %10
x /= 10

r = r * 10 + t
}

而后是添加判断溢出的方法,需要使用数学推导。

重点在于r * 10 + t这一句需要提前判断是否会导致溢出。

假设x为正数,则溢出时有10r+t>intMax。
但是我们不能直接这么写if,因为这样判断本身也会溢出,无法正确判断。
将等式同/10,此时便可以安全判断,r+t/10>intMax/10
但是注意又有新的问题,int会取整,导致/10后范围并不正确。

t的范围是0-9的整数,/10后为[0, 1),取整后得0.
intMax/10=214748364+0.7小数部分是原个位数字,取整后丢失小数,得214748364
若r>[intMax/10],就有r>intMax/10+x(x的范围[0,1)),由于x与t范围[0, 1)一致,也有r>intMax/10+t成立,溢出。
若r==[intMax/10],则还需判断t/10和丢失的小数部分0.7的关系,才能判断原式是否成立,也即t和原intMax的个位数字7的关系,若t>7则成立,溢出。

类似的,负数有
若r<[intMin/10],就有r>intMin/10+x(x的范围(-1, 0]),由于x与t范围(-1, 0]一致,也有r>intMax/10+t成立,溢出。
若r==[intMin/10],则还需判断t/10和丢失的小数部分-0.8的关系,才能判断原式是否成立,也即t和原intMax的个位数字-8的关系,若t<-8则成立,溢出。

总结:

  • 重视数学推导,思路不清晰时可使用数学方法。
  • 计算机的类型虽然有上限,但是数学推导的时候没有范围限制。因而,可以使用数学推导方法推导条件,然后通过变形式子,使得各中间值都落入计算机的表示范围内,如此绕过数值范围问题。
  • 处理此类数值的各位数字时,不必一开始就计算出各位的权值而一次性乘上,也可以通过循环叠加*10来达到目的。

CCF2018-12-3 CIDR合并

题目描述

题目主干

题目描述1
题目描述2
题目描述3

测试用例

样例输入
2
1
2
样例输出
1.0.0.0/8
2.0.0.0/8

样例输入
2
10/9
10.128/9
样例输出

10.0.0.0/8

样例输入
2
0/1
128/1
样例输出
0.0.0.0/0

评测用例规模与约定

评测用例规模与约定1
评测用例规模与约定2

解题要点

  1. 定义存储IP前缀的结构体,并在输入时完成标准化,有助于简化后续处理. 输出时则只需要输出标准格式.

  2. 排序时,由于STL list不是随机访问容器,不能使用<algorithm>库的sort(RandomIterator begin, RandomIterator end, Compare pred)
    但STL list容器自身提供了排序方法list.sort(Compare pred)

同时,由于是自定义结构,需要自行编写STL Compare,可以使用lambda表达式,也可以使用普通函数。
注意如何编写合法的STL Compare(需满足严格弱序、返回值代表第一个参数是否比第二个参数先出现)。

  1. 位运算相关知识。
    还可能涉及整型提升(Integer Promotion)。

  2. 对题目提示阅读理解及实现
    第二步从小到大合并,即为消除被其他前缀包含的子集前缀过程的过程。
    只要按较短的一个前缀的长度len,比较两个IP前缀的前len位是否相同,相同则代表长度较长的那个是长度较短的子集。
    由于排序保证后者范围更小,因此后者是子集,保留前者,删去后者。
    (此过程中也可以两前缀的长度相同,如果前len位相同,则代表两个前缀等价。
    由于排序保证后者地址更大,因此后者地址段包含无效数据更多,也是保留前者,删去后者)

第三步同级合并,即为将两个相同长度的前缀,合并为一个更短的前缀。
只要判断两个前缀的最后一个有效位不同,且前面len-1位相同,则可以两者合并为一个len-1长度的前缀。
可以用异或运算。

以上两步都要注意,由于在循环遍历中修改了遍历对象,无论对迭代器做前进还是后退更改,都要再次考虑迭代器的有效性、是否破坏了循环过程。

  1. 由于简化过程中需要大量修改容器内容,不要使用顺序容器vector等储存,
    否则修改时造成大量元素复制时间,后几个大规模测试数据中极易超时。
    推荐使用STL的list存储。

输入时,也可以预先申请空间而不是每次输入都调用push。

代码示例

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <cstdio>
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;

using IPAddr = unsigned int;
using Byte = unsigned char;
struct IPPrefix {
Byte ip[4];
short len;
bool isSubsetOf(const IPPrefix& o) {
if (this->len < o.len) {
return false;
}
auto lessLen = o.len;
IPAddr mask = 0;
for (int i = 0; i < lessLen; i++) {
mask |= 1U << (32 - (i+1));
}
IPAddr ip1 = this->getIP();
IPAddr ip2 = o.getIP();
IPAddr ip1subnet = ip1 & mask;
IPAddr ip2subnet = ip2 & mask;
return ip1subnet == ip2subnet;
}
inline IPAddr getIP() const {
return ip[0] << 24
| ip[1] << 16
| ip[2] << 8
| ip[3];
}
bool operator==(IPPrefix& rhs) const {
return this->getIP() == rhs.getIP()
&& this->len == rhs.len;
}
};

inline istream& operator>>(istream& is, IPPrefix& rhs) {
string input; is >> input;
string::size_type si = input.find_first_of('/');

string ipStr = input.substr(0, si);
string::size_type preDot = -1;
string::size_type nextDot = ipStr.find_first_of('.');
rhs.ip[0] = stoi(ipStr.substr(0, nextDot));
preDot = nextDot;
int sectionCnt = 1;
for (int i = 1; i < 4; i++) {
if (preDot == string::npos) {
rhs.ip[i] = 0;
}
else {
nextDot = ipStr.find_first_of('.', preDot + 1);
sectionCnt++;
string ipSection = ipStr.substr(preDot + 1, nextDot - (preDot + 1));
rhs.ip[i] = stoi(ipSection);
preDot = nextDot;
}
}

if (si == string::npos) {
rhs.len = 8 * sectionCnt;
}
else {
rhs.len = stoi(input.substr(si + 1));
}
return is;
}

inline ostream& operator<<(ostream& os, const IPPrefix& rhs) {
printf("%d.%d.%d.%d/%d", rhs.ip[0], rhs.ip[1], rhs.ip[2], rhs.ip[3], rhs.len);
return os;
}

inline void optimizeList(list<IPPrefix>& data) {
data.sort([](const IPPrefix& lhs, const IPPrefix& rhs) {
IPAddr ip1 = lhs.getIP();
IPAddr ip2 = rhs.getIP();
return ip1 < ip2
||
(ip1 == ip2 && lhs.len < rhs.len);
});
// 消除子集
for (auto i1 = data.begin(), i2 = ++data.begin(); i2 != data.end(); i1++, i2++) {
//clog << "compare1 " << *i1 << " & " << *i2 << endl;
while (i2!=data.end() && i2->isSubsetOf(*i1)) {
// clog << *i2 << " is subset of " << *i1 << ", erase former"<< endl;
i2 = data.erase(i2);
}
if (i2 == data.end()) {
break;
}
//clog << "compare1 " << *i1 << " & " << *i2 << endl;
}
// 合并同级
auto i1 = data.begin(), i2 = ++data.begin();
while (i2 != data.end()) {
if (i1->len == i2->len) {
IPAddr ip1 = i1->getIP();
IPAddr ip2 = i2->getIP();
auto len = i1->len;
// clog << "compare2 " << *i1 << " & " << *i2 << endl;
if ((ip1 ^ ip2) >> (32 - len) == 1U) // 前len-1位相同且第len位不同
{
// 合并
// clog << "merge " << *i1 << " &" << *i2 << endl;
i1->len--;
i2 = data.erase(i2);
// clog << "into " << *i1 << endl;
// 回退以检查合并产生的条目,与前一条目是否可以再次合并
if (i1 != data.begin()) {
// 若i1指向begin,则没有前一条,无需回退。
i1--;
i2--;
// 此处continue跳过了本次循环后面的自增操作。
continue;
}
else if (i2 == data.end()) {
// 由于减少一条目,此时i2可能已经到达表尾。
// 立即退出循环,否则再次i2自增会出错
break;
}
}
}
// 前进
i1++; i2++;
}
}

int main()
{
size_t n; cin >> n;
list<IPPrefix> list(n);
for (auto it = list.begin(); it != list.end(); it++) {
cin >> *it;
}
optimizeList(list);
for (auto it = list.cbegin(); it != list.cend(); it++) {
cout << *it << endl;
}
}

CCF2018-12-2 小明放学

题目描述

题目背景

  汉东省政法大学附属中学所在的光明区最近实施了名为“智慧光明”的智慧城市项目。具体到交通领域,通过“智慧光明”终端,可以看到光明区所有红绿灯此时此刻的状态。小明的学校也安装了“智慧光明”终端,小明想利用这个终端给出的信息,估算自己放学回到家的时间。

问题描述

  一次放学的时候,小明已经规划好了自己回家的路线,并且能够预测经过各个路段的时间。同时,小明通过学校里安装的“智慧光明”终端,看到了出发时刻路上经过的所有红绿灯的指示状态。请帮忙计算小明此次回家所需要的时间。

输入格式

  输入的第一行包含空格分隔的三个正整数 r、y、g,表示红绿灯的设置。这三个数均不超过 10^6。
  输入的第二行包含一个正整数 n,表示小明总共经过的道路段数和路过的红绿灯数目。
  接下来的 n 行,每行包含空格分隔的两个整数 k、t。k=0 表示经过了一段道路,将会耗时 t 秒,此处 t 不超过 10^6;k=1、2、3 时,分别表示出发时刻,此处的红绿灯状态是红灯、黄灯、绿灯,且倒计时显示牌上显示的数字是 t,此处 t 分别不会超过 r、y、g。

输出格式

  输出一个数字,表示此次小明放学回家所用的时间。

测试用例

样例输入

1
2
3
4
5
6
7
8
9
10
30 3 30
8
0 10
1 5
0 11
2 2
0 6
0 3
3 10
0 3

样例输出

1
46

样例说明
小明先经过第一段路,用时 10 秒。第一盏红绿灯出发时是红灯,还剩 5 秒;小明到达路口时,这个红绿灯已经变为绿灯,不用等待直接通过。接下来经过第二段路,用时 11 秒。第二盏红绿灯出发时是黄灯,还剩两秒;小明到达路口时,这个红绿灯已经变为红灯,还剩 11 秒。接下来经过第三、第四段路,用时 9 秒。第三盏红绿灯出发时是绿灯,还剩 10 秒;小明到达路口时,这个红绿灯已经变为红灯,还剩两秒。接下来经过最后一段路,用时 3 秒。共计 10+11+11+9+2+3 = 46 秒。

评测用例规模与约定

有些测试点具有特殊的性质:

  • 前 2 个测试点中不存在任何信号灯。

测试点的输入数据规模:

  • 前 6 个测试点保证 n ≤ 10^3。
  • 所有测试点保证 n ≤ 10^5。

解题要点

模拟题,比前一题稍微复杂,可在前一题基础上修改。

  1. 经过一个完整的红绿黄周期,状态相当于没有任何变化。
    因此通过对整个周期取余,可以大大节省运算时间。
    而后只要进行小于两个周期的模拟,即可得出结果。

  2. 注意模拟循环中:

->红->绿->黄->
对应的数字是
->1->3->2->
不要想当然认为是->1->2->3->,从而写错代码。

  1. 分析数据规模,可知:
  • r、y、g最大为10^6,n最大为10^5,使用int存储没有任何问题。
  • 但结果ans考虑最大的情况(全部遇到为黄灯刚开始)下,为ans=n*(y+r),10^5*(10^6+10^6)=2*10^11次方,
    不可以直接使用int、unsigned int,都达不到要求(可以使用的numeric_limits::min()/max()查询类型T的表示范围)。
    需要使用long long或者unsigned long long替换相关类型。
    如果没有注意数据规模问题,将导致最后两个测试点不通过。

代码示例

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <iostream>
#include <cstdio>
using namespace std;

using timeDuration = unsigned long long;

int stageTime[3];
// ->1->3->2->
void nextState(int &k) {
switch (k) {
case 1: // red
k = 3;
break;
case 2: // yellow
k = 1;
break;
case 3: // green
k = 2;
break;
}
}

void preState(int &k) {
switch (k) {
case 1: // red
k = 2;
break;
case 2: // yellow
k = 3;
break;
case 3: // green
k = 1;
break;
}
}

inline int getStageTime(const int& k) {
return stageTime[k-1];
}

void calcState(timeDuration time, int& k, timeDuration& t) {
const timeDuration cycle = stageTime[0] + stageTime[1] + stageTime[2];
time %= cycle;
if (time < t) {
t -= time;
return;
}
time -= t; nextState(k); // frist part stage
bool proceed = true;
timeDuration stageTime;
while (time > 0) {
stageTime = getStageTime(k);
if (time < stageTime) {
t = stageTime - time; // final part stage
break;
}
time -= stageTime; nextState(k); // proceed entire stage
}
return;
}

int main()
{
int &r = stageTime[0], &y = stageTime[1], &g= stageTime[2];
cin >> r >> y >> g;
timeDuration n; cin >> n;
timeDuration ans = 0;
for (timeDuration i = 0; i < n; i++) {
// clog << "当前时间点:" << ans << endl;
int k; timeDuration t; cin >> k >> t;
if (k == 0) {
ans += t;
continue;
}
// clog << "初始状态:" << k << " " << t << endl;
calcState(ans, k, t);
// clog << "达到状态:" << k << " " << t << endl;
switch (k)
{
case 1: // R
ans += t;
break;
case 2: // Y
ans += t + r;
break;
case 3: // G
ans += 0;
break;
default:
break;
}
}
cout << ans << endl;
return 0;
}

CCF2018-12-1 小明上学

题目说明

题目背景

  小明是汉东省政法大学附属中学的一名学生,他每天都要骑自行车往返于家和学校。为了能尽可能充足地睡眠,他希望能够预计自己上学所需要的时间。他上学需要经过数段道路,相邻两段道路之间设有至多一盏红绿灯。
  京州市的红绿灯是这样工作的:每盏红绿灯有红、黄、绿三盏灯和一个能够显示倒计时的显示牌。假设红绿灯被设定为红灯 r 秒,黄灯 y 秒,绿灯 g 秒,那么从 0 时刻起,[0,r) 秒内亮红灯,车辆不许通过;[r, r+g) 秒内亮绿灯,车辆允许通过;[r+g, r+g+y) 秒内亮黄灯,车辆不许通过,然后依次循环。倒计时的显示牌上显示的数字 l(l > 0)是指距离下一次信号灯变化的秒数。

问题描述

  一次上学的路上,小明记录下了经过每段路的时间,和各个红绿灯在小明到达路口时的颜色和倒计时秒数。希望你帮忙计算此次小明上学所用的时间。

输入格式

  输入的第一行包含空格分隔的三个正整数 r、y、g,表示红绿灯的设置。这三个数均不超过 106。
  输入的第二行包含一个正整数 n(n ≤ 100),表示小明总共经过的道路段数和看到的红绿灯数目。
  接下来的 n 行,每行包含空格分隔的两个整数 k、t。k=0 表示经过了一段道路,耗时 t 秒,此处 t 不超过 10^6;k=1、2、3 时,分别表示看到了一个红灯、黄灯、绿灯,且倒计时显示牌上显示的数字是 t,此处 t 分别不会超过 r、y、g。

输出格式

  输出一个数字,表示此次小明上学所用的时间。

测试用例

样例输入
30 3 30
8
0 10
1 5
0 11
2 2
0 6
0 3
3 10
0 3

样例输出
70

样例说明
  小明先经过第一段道路,用时 10 秒,然后等待 5 秒的红灯,再经过第二段道路,用时 11 秒,然后等待 2 秒的黄灯和 30 秒的红灯,再经过第三段、第四段道路,分别用时6、3秒,然后通过绿灯,再经过最后一段道路,用时 3 秒。共计 10 + 5 + 11 + 2 + 30 + 6 + 3 + 3=70 秒。

评测用例规模与约定

  测试点 1, 2 中不存在任何信号灯。
  测试点 3, 4 中所有的信号灯在被观察时均为绿灯。
  测试点 5, 6 中所有的信号灯在被观察时均为红灯。
  测试点 7, 8 中所有的信号灯在被观察时均为黄灯。
  测试点 9, 10 中将出现各种可能的情况。

解题要点

模拟题,非常简单,模拟情况即可。分析数据规模可知int即可表示所有情况。

注意,黄灯时间结束后还要等红灯结束,才是绿灯可通行。
因此遇到黄灯需要为消耗时间加上 黄灯剩余时间+红灯时间。

代码示例

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
#include <iostream>
using namespace std;

int main()
{
int r, y, g; cin >> r >> y >> g;
int n; cin >> n;
int ans = 0;
for (int i = 0; i < n; i++) {
// clog << "当前时间点:" << ans << endl;
int k, t; cin >> k >> t;
if (k == 0) {
ans += t;
continue;
}
// clog << "状态:" << k << " " << t << endl;
switch (k)
{
case 1: // R
ans += t;
break;
case 2: // Y
ans += t + r;
break;
case 3: // G
ans += 0;
break;
default:
break;
}
}
cout << ans << endl;
return 0;
}

关于软件工程过度设计的一点感想

代码架构设计的复杂性就像一个左右摇摆的天平。
过于简单,则越有可能产生不好的代码,增大代码修改的难度(需要理解原有不良代码),降低代码维护性。
过于复杂,则产生无用的抽象层,增大代码修改的复杂度(需要理解原有抽象设计并保持),同样会降低代码维护性。
过度设计,其实就是把维护烂代码吃的屎,转成了维护烂设计吃的屎。

有个关键的问题:对于一个设计,想让它被使用起来越爽,相应要去维护它也越累。
我们有时常常无视了设计带来的维护复杂度,只看到了它的使用便利性。我们为软件工程的各种设计而兴奋,梦想着在良好设计的代码库中高效工作而兴奋。

实际上,天下没有白吃的午餐。必须认识到维护设计也有成本,势必会有人为此承担债务,但不代表软件设计就毫无用处,适当的设计可以使使用便利性的好处高于维护复杂性的坏处。

业务代码在设计上要更多地权衡利弊,因为使用者和维护者是同一主体,两者此消彼长,但却也不是等额增减。争取设计带来的使用便利性大于维护复杂性。如果两者相等,设计便是无用的。如果使用便利性小于维护复杂度,那这个起反作用的过度设计应当尽快被移除。
特别地,对于基础通用类库,与业务代码不同,抽象设计的使用者和维护者不是同一批人,值得在设计的天平上,比业务代码更多地倾向良好设计。基础类库的维护者可以通过好设计,将广大使用者的债务负担,转移至维护者一次性解决或削减。

博客重建说明

旧博客的源文件已经完全丢失,故重建博客。

关于旧文章

原博客迁移至域名old.blog.moew.xyz,继续保留开放,但是无意外不会再做修改。

有价值的文章后续会转移过来,这边也会陆续完善。

关于友情链接

已知需要更新的友情链接,已经全部更新完毕。

有几个站无法访问,又联系不到人,只好撤除链接。
如果站点还在需要重新添加,可以联系我。

转换罗马数字

基本思路:
由于最大单位为由M表示的1000,所以千、百、十、个的各个数值均有有限的固定组合且数量不大。
进而1000以下的组合可通过查表拼接完成转换,1000以上部分可通过动态生成重复M的拼接。

首先通过取余循环分离各位数字,而后计算索引值并查表取出对应字符串进行拼接,最后返回结果

JS实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    function convert(num) {
var rest = num;
var numArr = [];
var table = {
0:'',

1:'I', 2:'II', 3:'III', 4:'IV', 5:'V',
6:'VI', 7:'VII', 8:'VIII', 9:'IX',

10: 'X', 20:'XX', 30:'XXX', 40:'XL', 50:'L',
60:'LX', 70:'LXX', 80: 'LXXX', 90: 'XC',

100:'C', 200: 'CC', 300:'CCC', 400: 'CD', 500:'D',
600:'DC', 700:'DCC', 800: 'DCCC', 900: 'CM',

1000:'M'
};
//分离各位数字
while(rest > 0){
numArr.push(Math.floor(rest % 10));
rest = Math.floor(rest / 10);
}
console.log(numArr);
var result = numArr.reduce(function(str, val, index){
//index代表原始数据从个位起索引
if (index < 3){
//1000以下部分查表转换并拼接
return table[val*Math.pow(10,index)] + str;
}else{
//1000以上部分动态生成
var unit = table[1000].repeat(Math.pow(10,index-3));
return unit.repeat(val) + str;
}
}, '');
console.log(result);
return result;
}

//test case
console.assert(convert(2)==="II");
console.assert(convert(9)==="IX");
console.assert(convert(12)==="XII");
console.assert(convert(29)==="XXIX");
console.assert(convert(500)==="D");
console.assert(convert(501)==="DI");
console.assert(convert(649)==="DCXLIX");
console.assert(convert(891)==="DCCCXCI");
console.assert(convert(999)==="CMXCIX");
console.assert(convert(1000)==="M");
console.assert(convert(1004)==="MIV");
console.assert(convert(2017)==="MMXVII");
console.assert(convert(3999)==="MMMCMXCIX");
console.assert(convert(10000)==="MMMMMMMMMM");
console.assert(convert(16234)==="MMMMMMMMMMMMMMMMCCXXXIV");
console.assert(convert(316234)==="MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCCXXXIV");

参考资料:Roman Numerals

【20170710】C++ 初学踩过的一些坑总结

关于double与float

字面值默认是double, 加f才是float
如124412.0是double。124412.0f才是float
没特殊需要只用double(现代CPU可直接运算,性能与存储速度不比float慢),特殊情况考虑float和long double等

类型转换

1 / 2 = 0 // both int
1.0 /2 = 0.5 // double and int
1 / 2.0 = 0.5 // int and double

io流操作符setw()

setw()只对紧随其后的输出起作用,其他的操作如setfill()对随后的所有输出起作用。

cout << setfill('0')<< setw(5) << 233 << 666;

将输出

00233666

关于类的数据成员初始化代码位置问题

有且只有static成员可以且必须在类外初始化,非staitc只能在类内初始化。

非const不可以直接在声明后写初值
const数据成员不可以在构造函数体中赋值。
函数实现不管是否staitc/const可以在任何地方写函数体,但在类内会默认当做inline函数

乱七八糟的……画了个流程图结合下面示例看一下吧

流程图包含4种位置(类外初始化、声明处初始化、构造函数初始化列表处初始化、构造函数体赋值),没有出现的即为不可以。

其中构造函数体中代码,严格来讲不是初始化而是赋值,但是对于一些内置类型来说也相差无几。

关于类的数据成员初始化代码位置问题

类外初始化示例:

class c
{
private:
     static int x_;
public: 
     c(){}; 
};
static int c::x_ = 666; // correct

类内声明处初始化示例:

class c
{
private:
     const int x_ = 666; // correct
public: 
     c(){}; 
};

初始化列表示例:

class c
{
private:
     const int x_; 
public: 
     c():x_(666){}; // correct
};

函数体示例:

class c
{
private:
     int x_; 
public: 
     c():{
        this->x_ = 666; // correct
     }; 
};

堆上动态分配的类数据成员

应该定义拷贝构造函数与赋值运算符函数,否则按编译器默认行为可能造成出错(默认浅拷贝行为)

myClass(const myClass& anotherObject)
{
     // run when constructing object (this is still uninitialized)……
}

myClass& operator=(const myClass& anotherObject)
{
    // run when be assigned (this is already initialized)……
}

const对象与非const对象可以相互转换

 const int a =0;
 int b;
 b = a; //correct
 const int c = b; //correct

对于类来说,非const对象可以直接转换为const对象,而const对象转换为非const对象则需要对应的接收const对象的复制构造函数(默认会有一个,但是如果已有复制构造函数但不接收const对象则会导致无法转换)

友元函数

声明友元函数时,即使并在头文件中不使用具体类型,也要include具体文件以引入具体类型声明。

类继承体系

非虚函数不要在子类中重定义
原因:如果需要重定义,那么就不应该继承,子类与基类不构成is-a关系
非虚函数静态联编,使基类引用总调用基类函数,即使这个基类引用实际引用了一个派生类对象

从基类继承的带默认参数的函数,不要重定义其参数

重载函数注意事项

重载类构造函数

派生类永远在初始化列表中显式调用基类构造函数
复制构造函数还要注意参数要接收const对象以提供const对象到非const对象的转换

重载类型转换函数

没有返回值,且必须是非静态成员函数

//correct
myClass::operator double()
{
    return this.toDouble();
}

//error
static myClass::operator double()
{
    //……
}

//error
operator double(const myClass &object)
{
    return object.toDouble();
}

重载赋值运算符

永远应该返回*this以便使用于连续赋值
永远应该检查到自身的赋值以防错误
派生类永远显示调用基类赋值运算符,若基类没有重载赋值运算符,则应 static_cast<base_class>(*this)=rhs;
参数要接收const对象以提供const对象到非const对象的转换

const myClass& operator=(const myClass& rhs)
{
    base_class::operator=(rhs); // or static_cast<base_class>(*this)=rhs;
    if(this != &rhs)
    {
         // do something
    }
    return *this;
}
myClass c1,c2,c3;
c1 = c2 = c3;

运算符==与!=重载

总应该同时重载两者,且执行结果相反。

重载数组下标运算符

返回值类型需为非const引用类型,以便进行c[i]=xxx;的赋值操作

重载&& || ,

不要试图重载&& || ,
重载后他们便成为了普通函数,无法保证从左到右求值,&&和||使用的短路求值也无法保证。

异常处理

避免在构造函数中抛出异常
因为不会调用析构函数,而可能导致已经分配的资源无法释放,造成内存泄漏。
如果一定要抛出也要保证会安全释放,譬如使用智能指针。

catch类型最好是引用类型
对于类对象来说,引用不仅节约了拷贝对象的时间,而且防止可能异常对象会复制失败。
同时可以利用多态,而防止对象截断。

dynamic_cast操作的对象是类指针
bad_cast只有在无法相互转换的类进行dynamic_cast时才会抛出,如同级的互相转换。
如指向派生类对象的基类指针试图转换到另一派生类的指针,即试图向下类型转换(从父类转换为子类)失败,则会返回空指针而不会抛出bad_cast。

STL中异常基类及一系列派生类在<exception>中定义,而runtime_error与logic_erro还需要<stdexcept>

函数模板

在试图进行类型匹配时,不会进行类型转换。
即(T a, T b)只匹配(int, int)(double, double)等相同类型,而不会进行类型转换以匹配(int, double)
模板参数只接受编译期可确定的常量值

template <typename T>
void func(T a, T b)
{}
func(1, 2); //legal
func(1.0, 2.0); //legal
func(1.0, 2); //illegal

若为类成员函数,则需将定义放在头文件.h中以编译期实例化。

类模板

默认类型参数

可以为类模板指定默认类型参数

template <typename T = int>

但不能在函数模板中使用默认类型参数。

继承限制

类模板可以继承普通类。
类模板可以继承类模板。
普通类可以继承类模板的特化。
普通类不可以继承类模板。
总结:若子类为已实例化的类,则基类必须已实例化。

静态成员

每个模板实例类都有一份静态数据成员

嵌套类型

一个模板中的依赖于模板参数的名字被称为依赖名字
当一个 依赖名字 嵌套在一个类的内部时,我称它为 嵌套依赖名字
嵌套依赖名字必须显式使用typename关键字声明其为类型,否则将不被认为是类型而可能导致编译错误。

例如:

template<typename T> void f() {
    T::c* x;
   ...
}

这里没有使用typename声明,我们想表达的是:定义一个指针变量x,指向的数据类型是类T中的c类型。
然而编译器将认为是T中的静态成员变量c与已存在的变量x进行*运算相乘,此时编译出错。

如果要声明x为C内的nested dependent name(嵌套依赖名字),必须显式使用typename:

template<typename T> void f() {
    typename T::c* x;
   ...
}
  • Copyrights © 2014-2022 听寒
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信