大二上学期,进入物联网专业学习。
这次学的 C++ 工作量陡然提升,实验日晚上当场写几百行代码已经是常态化了。
下面按照惯例总结一下。
引用
给变量起别名
int a = 0;
int &b = a;
面向对象编程时,通过引用传递对象时可以避免传入的对象被重复构造。
但是对于返回值为引用型的函数,不可以将局部变量返回。
返回一个局部变量的引用错误,局部变量离开了定义它的{}就应该失效了,有时候能用只是因为旧的内存还没有被其他函数使用
zp老师
返回引用的目的就是希望在调用函数中能够继续使用它,因此返回的引用对象在调用函数中也应该是有效的,所以返回引用的函数只能返回
全局有效对象(全局对象,类的静态成员之类的),传进来的引用类形参(包含*this)
指针与堆内存的分配
分配单个变量
int *p = new int(1);
分配一个数组
int *p = new int[N];
分配一个指针数组
int *p = new int * [N];
动态二维数组
int **p = new int * [m];
for (int i = 0; i < m; i++) {
p[i] = new int[n];
for (int j = 0; j < n; j++) {
// 可对 p[i][j] 进行访问、赋值
}
}
动态二维数组的内存释放
for (int i = 0; i < m; i++) {
delete [] p[i];
}
delete [] p;
类与对象
静态成员
Q: 为什么需要静态成员?
A: 静态成员可以实现在多个对象之间共享数据
class Foo {
public:
static int num;
};
// 必须在全局区初始化
int Foo::num = 1;
静态成员变量不存储在本对象的内存区中,故不能使用 this
指针或对象指针来访问静态变量
对象数组
class Foo {
public:
Foo() {
// 构造函数在这里实现
}
};
Foo a[10];
上述代码会创建一个长度为 10,类型为 Foo 的数组,它的每一个成员都是一个已初始化的 Foo 对象。
使用这个方法会默认调用 Foo(); 的构造函数进行构造,有时候为了实现传入参数的构造,我会使用类似创建动态数组的方法。
Foo **p = new Foo * [m];
for (int i = 0; i < m; i++) {
p[i] = new Foo();
// p[i] 是一个指针,访问成员时候使用 -> 操作符;
}
友元
为友元函数、类添加访问 protected
和 private
的成员
友元函数
class Foo {
private:
int a;
public:
friend void test(Foo& f);
};
void test(Foo& f) {
// 可以访问 f.a;
}
友元类
class Bar;
class Foo {
private:
int a;
public:
friend class Bar;
};
class Bar {
public:
void getFooA(Foo f) {
// 可以访问 f.a;
}
};
构造与析构
构造
class Foo {
public:
Foo() {
// 构造函数在这里实现
}
Foo(int, double, ...) {
// 重载构造函数
}
};
(深)拷贝构造与构造
class Foo {
private:
char buffer[N];
char *p;
public:
Foo() {
// 构造函数在这里实现
}
Foo(Foo& f) {
// 拷贝构造函数在这里实现
strcpy(buffer, f.buffer);
p = new char[strlen(f.p)+1];
strcpy(p, f.p);
}
~Foo() {
delete [] p;
}
};
对于自己申请开辟的堆区空间,应在析构函数中进行释放。
继承
先定义一个基类
class Base {
private:
int c;
protected:
int b;
public:
int a;
};
继承以及派生类对变量的访问权限
class A : public Base {
// 可以访问 a, b
};
class B : protected Base {
// 可以访问 a, b
};
class C : private Base {
// 可以访问 a, b
};
外部的类对派生类内变量的访问权限
class Out {
public:
void test(A& a, B& b, C& c) {
// 只可以访问 a.a;
}
};
多重继承与虚继承
多继承
class A {};
class B {};
class C : public A, public B {};
菱形继承与虚继承
class Base {
protected:
int m_base;
public:
Base(int base): m_base(base) {}
};
class A : public Base {
protected:
int m_a;
public:
A(int base, int a) : Base (base), m_a(a) {}
};
class B : public Base {
protected:
int m_b;
public:
B(int base, int b) : Base (base), m_b(b) {}
};
class C : public A, public B {
public:
C(int base, int a, int b) : A(base, a), B(base, b) {}
void test() {
// A::base 与 B::base 在这里是等价的
}
};
使用多继承来解决「菱形继承」问题有一些问题,比如在访问基类 Base 内的成员时,如果直接使用 Base::base
或 base
或者 A::Base::base
可能会在某些版本的 g++ 中报错
error: 'Base' is an ambiguous base of 'C'
这里是产生了二义性的问题,虽然通过访问 A::base
, B::base
可以解决,但在继承的过程中,相同的数据被储存了两次,内存洁癖直呼难顶。
这个时候就需要用虚继承来解决了(好吧我承认考前两天才掌握这个新方法)
class Base {
protected:
int m_base;
public:
Base(int base): m_base(base) {}
};
class A : virtual public Base {
protected:
int m_a;
public:
A(int base, int a) : Base (base), m_a(a) {}
};
class B : virtual public Base {
protected:
int m_b;
public:
B(int base, int b) : Base (base), m_b(b) {}
};
class C : public A, public B {
public:
C(int base, int a, int b) : Base(base), A(base, a), B(base, b) {}
void test() {
// 可以直接访问 base 或者 Base::base;
}
};
通过虚基类,我们就可以解决多继承中重复继承基类的问题。
值得说明的时,使用虚基类这个技术只影响了从虚基类的派生类中进一步派生出来的类(C),而基类的派生类(A,B)本身并不会被影响。
虚函数与多态
教授说这个是 C++ 的经典技术。
虚函数
#include <iostream>
using namespace std;
class Foo {
public:
virtual void print() {
cout << "Foo" << endl;
};
};
class Bar : public Foo {
public:
void print() {
cout << "Bar" << endl;
};
};
int main() {
Foo *p;
Foo f;
Bar b;
p = &f;
p->print(); // output: Foo\n
p = &b;
p->print(); // output: Bar\n
return 0;
}
纯虚函数
在上述例子中,如果基类中的 print();
没啥实际意义也不需要被调用,但会在派生类重新定义这个函数的时候,可以不实现基类中的虚函数,直接改用纯虚函数。
class Foo {
public:
virtual void print() = 0;
};
多态
在虚函数的例子中,我们通过基类的指针实现了多态
这样一来,我们只需要将派生类的地址赋给基类的指针,就可以通过基类的指针操作多种派生类内部相同名字的成员,或者是调用相同参数的函数,这比函数重载能节省更多的代码。
运算符重载
教授说这也是 C++ 的精髓,不过确实,重载这玩意有点东西
可重载的运算符如下
双目运算 | + – * / % |
单目运算 | +(正), -(负), *(指针), &(取地址) |
逻辑运算 | ||, &&, ! |
关系运算 | ==, !=, <, >, <=, >= |
自增自减运算 | ++, — (pre, post) |
位运算 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
#include <iostream>
using namespace std;
class Foo {
public:
int a;
int b;
Foo(int _a, int _b): a(_a), b(_b) {};
int operator+(Foo & f) {
return a*b + f.a*f.b;
};
// 前置++, --
int operator++() {};
int operator--() {};
// 后置++, --
int operator++(int) {};
int operator--(int) {};
};
int main() {
Foo a(1,1), b(2,2);
cout << a + b << endl; // output: 5\n
return 0;
}
输出重载
#include <iostream>
using namespace std;
class Foo {
public:
int a;
int b;
Foo(int _a, int _b): a(_a), b(_b) {};
friend ostream& operator<<(ostream& cout, Foo& f);
};
ostream& operator<<(ostream& cout, Foo& f) {
return cout << "a:" << f.a << " b:" << f.b << endl;
}
int main() {
Foo f(1,1);
cout << f << endl; // output: a:1 b:1\n
return 0;
}
函数模板
逻辑相同,参数类型不同可以使用函数模板来处理,这样也可以比重载使用更少的代码。
template<typename T>
T max(T a, T b) {
retrun a > b : a ? b;
}
上述代码在被调用时,不管是 int
, double
, 或者是 char
, string
都可以被比较,T 的类型由编译器自动推导,但是传入的 a 与 b 的类型必须是相同的。
类模板
也可以用来处理不同类型的变量
#include <iostream>
using namespace std;
template<typename T_a, typename T_b, int Length>
class Test {
T_a a;
T_b b;
int buffer[Length];
public:
Test(T_a _a, T_b _b): a(_a), b(_b) {}
void print() {
cout << "a: " << a << " b:" << b << endl;
cout << "Length of buffer[] is " << sizeof(buffer) / sizeof(int) << endl;
}
};
int main() {
Test<string, int, 10> t("Hello", 233);
t.print(); // output: a: Hello b:233\nLength of buffer[] is 10
return 0;
}
使用 Stringstream
C++ 的流是在是太爽了,特别是用于 string(char) 与 int 互转的时候,简直不要太爽。
void test() {
string v1, v2;
streamstring ss;
ss << 1;
ss >> v1; // int(1) 被转入 string(1)
ss.clear(); // 再次使用时需要先清空
ss << 2;
ss >> v2; // int(2) 被转入 string(2)
}
下面是利用 stringstream 写的四进制加法器
#define N 16
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
class QuaternaryInt {
string value;
public:
QuaternaryInt(string v = "0"): value(v) {
while (value.length() < N) {
value = "0" + value;
}
}
QuaternaryInt operator+(QuaternaryInt &a) {
string v, tmp;
stringstream ss;
int next = 0, current = 0;
for (int i = N - 1; i >= 0; i--) {
current = value[i] + a.value[i] - 2 * '0' + next;
if (current > 3) {
next = current / 4;
current %= 4;
} else {
next = 0;
}
ss.clear();
ss << current;
ss >> tmp;
v = tmp + v;
}
return QuaternaryInt(v);
}
friend ostream& operator<<(ostream& cout, QuaternaryInt &a);
};
ostream & operator<<(ostream& cout, QuaternaryInt &a) {
int i;
for (i = 0; i < N - 1; i++) {
if(a.value[i]!='0')break;
}
return cout << a.value.substr(i, N);
}
int main() {
int t;
cin >> t;
QuaternaryInt a;
while (t--) {
string v;
cin >> v;
QuaternaryInt b(v);
a = a + b;
}
cout << a << endl;
return 0;
}
链表(没学)
这次不考,大概的定义方法就是这样
class List {
public:
int a;
List *next;
};
需要注意的是,在处理两个链表相加的过程中,不要直接修改其中一个链表的头结点内的 next 指针,这样做就等于只加了一个节点,自己把后面的关系都断掉了。
下面给出两个代码片段,展示的是列表的合并和列表的迭代搜索
Info * Find(string name) {
int offset = name[0] - 'A';
if (offset < 0 || offset > hmax) return NULL;
for (auto i = &Table[offset]; i->next != NULL; i = i->next) {
if (i->getName() == name) return i;
}
return NULL;
}
void Add(string name, int pno) {
auto f = Find(name);
if (f) {
f->setPhoneNo(pno);
} else {
int offset = name[0]-'A';
Info * next = new Info(Table[offset].getName(),
Table[offset].getPhoneNo(), Table[offset].next);
Table[offset] = Info(name, pno);
Table[offset].next = next;
}
}
void Merge(PhoneBook& pb) {
for (int i = 0; i < hmax; i++) {
for (auto j = &pb.Table[i]; j->next != NULL; j = j->next) {
Add(j->getName(), j->getPhoneNo());
}
}
}
在处理两个链表合并的过程中,我把新加入的节点丢进了堆区。
map
#include <map>
using namespace std;
// 键的类型 值的类型
map<int, string> trans_status = {
{1, "在用"},
{2, "未用"},
{3, "停用"}
};
string
#include <string>
using namespace std;
string v;
常用函数
判断字符串相等可直接使用 == 运算符,赋值使用 = 运算符
.size()
和 .length()
返回字符串的长度
一些算法
日期差值计算
#include <iostream>
using namespace std;
class Student {
public:
string name;
int year;
int month;
int day;
Student(string n, int y, int m, int d)
: name(n), year(y), month(m), day(d) {}
bool isLeapYear() {
return (year % 4 == 0 && year % 100) || year % 400 == 0;
}
bool isLeapYear(int y) {
return (y % 4 == 0 && y % 100) || y % 400 == 0;
}
int *getDaysOfMonth() {
static int daysOfMonth[] = {31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
if (isLeapYear()) {
daysOfMonth[1] = 29;
} else {
daysOfMonth[1] = 28;
}
return daysOfMonth;
}
int getDaysOfYear() {
return isLeapYear() ? 366 : 365;
}
int getDaysOfYear(int i) {
return isLeapYear(i) ? 366 : 365;
}
int getDaysFromYearStart() {
int *daysOfMonth = getDaysOfMonth();
int sum = day;
for (int i = 0; i < month - 1; i++) {
sum += daysOfMonth[i];
}
return sum;
}
int operator-(Student &s) {
auto a = -getDaysFromYearStart();
for (int i = year; i < s.year; i++) {
a += getDaysOfYear(i);
}
auto b = s.getDaysFromYearStart();
return a + b;
}
int dateToInt() {
return year * 1000 + month * 100 + day;
}
bool operator>(Student &s) {
return dateToInt() > s.dateToInt();
}
};
int main() {
int t, y, m, d;
string name;
cin >> t;
Student **p = new Student *[t];
for (int i = 0; i < t; i++) {
cin >> name >> y >> m >> d;
p[i] = new Student(name, y, m, d);
}
for (int i = 0; i < t; i++) {
for (int j = 0; j < t - i - 1; j++) {
if (*p[j + 1] > *p[j]) {
Student *tmp = p[j + 1];
p[j + 1] = p[j];
p[j] = tmp;
}
}
}
cout << p[t - 1]->name << "和" << p[0]->name << "年龄相差最大,为"
<< *p[t - 1] - *p[0] << "天。" << endl;
return 0;
}
循环时钟
#include <iostream>
using namespace std;
class Timer {
int h, m, s;
public:
Timer(int _h, int _m, int _s) : h(_h), m(_m), s(_s) {}
Timer operator++() {
++s;
if (s > 59) {
s %= 60;
m++;
}
if (m > 59) {
h++;
m %= 60;
}
if (h > 11) {
h %= 12;
}
return Timer(h, m, s);
}
Timer operator--(int) {
s--;
if (s < 0) {
s = 60 + s;
m--;
}
if (m < 0) {
h--;
m = 60 + m;
}
if (h < 0) {
h = 12 + h;
}
return Timer(h, m, s);
}
friend ostream &operator<<(ostream &cout, Timer &t);
};
ostream &operator<<(ostream &cout, Timer &t) {
cout << t.h << ":" << t.m << ":" << t.s;
return cout;
}
int main() {
int h, m, s, t;
cin >> h >> m >> s >> t;
Timer c(h, m, s);
while (t--) {
cin >> s;
if (s > 0) {
for (int i = 0; i < s; i++) {
++c;
}
} else {
for (int i = 0; i < -s; i++) {
c--;
}
}
cout << c << endl;
}
return 0;
}
后记与总结
本文出现的部分OJ题目代码在 https://github.com/0xJacky/cpp-oj
这次使用的 Python 对写在注释里的题目转换为了 Markdown 格式,欢迎在 GitHub 的仓库内查阅。
C++ 面向对象程序设计比去年的 C 程序设计的难度高了一大截,暂且不说算法难度的提升,题目描述就非常的长,几乎每一题都是原来 C 程序设计压轴题的长度(甚至更长)。因此,熟练掌握 STL 容器可能可以帮助节省代码,毕竟很多东西是不用自己重复造轮子的,写的越精练,debug 的难度就越小。
评论 (0)