夜间模式
C++学习笔记
内存模型
C++程序在执行时,将内存大致分为4个区域。
- 代码区:存放二进制代码,由操作系统管理。
- 全局区:存放全局常量、全局变量以及静态变量。
- 栈区:由编译器分配和释放,存放局部变量、局部常量等。
- 堆区:由程序员分配和释放。
程序编译后运行前出现代码区、全局区,运行后出现栈区、堆区。
可以通过 new
关键字开辟内存到堆区,通过 delete
关键字可以释放。
c++
int* p;
p = new int(10);
delete p;
p = new int[5] { 1, 2, 3, 4, 5 };
delete[] p;
引用
相当于给变量起别名。本质上是一个指针常量 int * const p
。
c++
int a = 10;
int& b = a;
b = 20; // 通过b改变a
cout << a; // 输出: 20
cout << (&a == &b) << endl; // 输出: 1。a与b地址完全相同。
WARNING
引用声明的同时必须初始化。
函数重载
函数重载的条件:
- 同一个作用域下
- 函数名相同
- 函数的参数类型不同、个数不同、顺序不同。
c++
void f() { cout << "f()" << endl; }
void f(int n) { cout << "f(int n)" << endl; }
void f(char c) { cout << "f(char c)" << endl; }
void f(int n, char c) { cout << "f(int n, char c)" << endl; }
void f(char c, int n) { cout << "f(char c, int n)" << endl; }
int main() {
f(); // 输出:f()
f(1); // 输出:f(int n)
f('a'); // 输出:f(char c)
f(1, 'a'); // 输出:f(int n, char c)
f('a', 1); // 输出:f(char c, int n)
return 0;
}
类和对象
C++面向对象的三大特性:封装、继承和多态。
访问权限有三种:
- 公共权限:
public
- 保护权限:
protected
(类外不能访问) - 私有权限:
private
(类外不能访问,子类不能访问)
c++
class Student {
private:
string m_name;
public:
void setName(string name) {
m_name = name;
}
string getName() {
return m_name;
}
};
TIP
在C++中,结构体 struct
默认为公共权限,类 class
默认为私有权限。
构造函数:类名() {}
- 无返回类型,无返回值
- 可以有参数,可重载
- 对象创建时自动调用
析构函数:~类名() {}
- 无返回类型,无返回值
- 无参数,不可重载
- 对象销毁时自动调用
c++
class Student {
public:
// 构造函数
Student() { cout << "Student()" << endl; }
Student(int n) { cout << "Student(int n)" << endl; }
// 拷贝构造函数
Student(const Student &p) { cout << "Student(const Student &p)" << endl; }
// 析构函数
~Student() { cout << "~Student()" << endl; }
};
int main() {
Student stu1; // 调用默认构造函数不要加小括号。否则会被当成函数声明
// 括号法
Student stu2(1);
Student stu3(stu1);
// 显式法
Student stu4 = Student();
Student stu5 = Student(1);
Student stu6 = Student(stu1);
Student(1); // 匿名对象。该行代码执行完,对象直接销毁。
// 隐式法
Student stu7 = 1; // 相当于写了 Student stu7 = Student(1);
Student stu8 = stu1; // 相当于写了 Student stu8 = Student(stu1);
return 0;
}
TIP
- 如果不写构造函数,C++会自动添加默认构造函数、拷贝构造函数。
- 如果写了有参构造函数,C++不会自动添加默认无参构造函数,但是会提供默认拷贝构造函数。
- 如果写了拷贝构造函数,C++不会自动添加其他构造函数。
初始化列表
c++
class Person {
public:
int m_Id;
int m_Age;
// 初始化列表
Person(int id, int age) : m_Id(id), m_Age(age) {}
// 相当于:
// Person(int id, int age) {
// m_Id = id;
// m_Age = age;
// }
};
静态成员变量
关键字:static
用法:类内声明,类外初始化
特点:所有对象共享同一份数据
c++
class Person {
public:
static int m_Num; // 类内声明
};
int Person::m_Num = 100; // 类外初始化
int main() {
cout << Person::m_Num << endl; // 通过类名访问
cout << Person().m_Num << endl; // 通过对象访问
return 0;
}
静态成员方法
关键字:static
用法:类内声明
注意:静态成员方法不可以访问非静态成员变量
c++
class Person {
public:
static void func() {
cout << "static void func()" << endl;
}
};
int main() {
Person::func(); // 通过类名访问
Person().func(); // 通过对象访问
return 0;
}
对象大小
空对象占一个字节的空间。
只有非静态成员变量才占对象空间。
所有对象共享一个非静态成员方法的实例,通过关键字 this
区分具体哪个对象。
c++
class Person1 {};
class Person2 {
int a; // 非静态成员变量, 占对象空间
static int b; // 静态成员变量,不占对象空间
void func1() {} // 非静态成员方法,不占对象空间
static void func2() {} // 静态成员方法,不占对象空间
};
int main() {
cout << sizeof(Person1) << endl; // 输出:1
cout << sizeof(Person2) << endl; // 输出:4
return 0;
}
this
指针
所有同类型对象会共用一个非静态成员函数实例,那么该函数如何区分是哪个对象调用了自己呢?通过 this
指针。
用途:
- 形参和成员变量同名时,可用
this
区分。 - 需要返回对象本身时,可用
return *this
。
c++
class Person {
public:
int age;
Person(int age) {
this -> age = age;
}
};
int main() {
cout << Person(18).age << endl;
return 0;
}
常函数
常函数:
- 成员函数声明后面加
const
称为常函数 - 常函数不可以修改成员变量
- 成员变量声明时加关键字
mutable
后,则可被常函数修改
常对象:
- 声明对象前加
const
,该对象为常对象 - 常对象只能调用常函数
c++
class Person {
public:
int mutable m_Age; // mutable修饰的成员变量可被常函数修改
Person(int age) : m_Age(age) {}
void addAge() const { // 声明常函数
this->m_Age++;
}
};
int main() {
const Person p(10); // 声明常对象,常对象只能调用常函数
p.addAge();
return 0;
}
友元
友元可以让私有属性被类外的类或函数访问到。关键字:friend
c++
class Person {
friend void printAge(Person); // 类做友元
friend class Friend; // 函数做友元
public:
Person() :age(10) {}
private:
int age;
};
class Friend {
public:
Person m_p;
Friend(Person p) :m_p(p) {};
void printAge() { // 类访问其他类私有属性
cout << "类做友元:" << m_p.age << endl;
}
};
void printAge(Person p) { // 函数访问类的私有属性
cout << "函数做友元:" << p.age << endl;
}
int main() {
Person p;
printAge(p); // 输出:函数做友元:10
Friend(p).printAge(); // 输出:类做友元:10
return 0;
}
运算符重载
通过成员函数重载:
c++
class Person {
public:
int m_age;
Person(int age) :m_age(age) {}
int operator+ (Person other) {
return this->m_age + other.m_age;
}
};
int main() {
Person p1(20);
Person p2(30);
int totalAge = p1 + p2;
cout << totalAge << endl; // 输出:50
return 0;
}
通过全局函数重载:
C++
class Person {
public:
int m_age;
Person(int age) :m_age(age) {}
};
int operator+ (Person p1, Person p2) {
return p1.m_age + p2.m_age;
}
int main() {
Person p1(20);
Person p2(30);
int totalAge = p1 + p2;
cout << totalAge << endl; // 输出:50
return 0;
}
左移运算符重载
重载后可输通过 cout << obj;
自定义输出 obj
的相关信息。类似于 Python 的魔术方法 __str__
。
不可以使用成员函数重载 << 运算符
。因为其方法 operator<< (cout)
简化后为 p << cout
,无法实现 cout
在运算符左边。所以要用全局函数重载运算符。
C++
class Person {
public:
int m_age;
};
ostream& operator<< (ostream& cout, Person p) {
cout << "Age: " << p.m_age;
return cout;
}
int main() {
Person p;
p.m_age = 10;
cout << p << endl; // 输出:Age: 10
return 0;
}
递增运算符重载
C++
class Person {
public:
void operator++ () {} // 前置递增, ++obj
void operator++ (int) {} // 后置递增, obj++
};
关系运算符重载
C++
class Person {
public:
int m_age;
Person(int age) :m_age(age) {}
bool operator< (Person& other) { return m_age < other.m_age; }
bool operator> (Person& other) { return m_age > other.m_age; }
bool operator== (Person& other) { return m_age == other.m_age; }
bool operator<= (Person& other) { return m_age <= other.m_age; }
bool operator>= (Person& other) { return m_age >= other.m_age; }
bool operator!= (Person& other) { return m_age != other.m_age; }
};
int main() {
Person p1(10), p2(20);
cout << (p1 < p2) << (p1 > p2) << (p1 == p2)
<< (p1 <= p2) << (p1 >= p2) << (p1 != p2); // 100101
return 0;
}
函数调用符重载
由于重载后的使用方式非常像函数的调用,因此称为 仿函数
。
C++
class Person {
public:
int m_age;
Person(int age) :m_age(age) {}
void operator() (string text) {
cout << text << endl;
}
};
int main() {
Person p(10);
p("Hello World"); // 输出:Hello World
return 0;
}
继承
C++
class Person {
public:
string m_Name;
};
class Student :public Person {
public:
void learn() {
cout << m_Name << " is learning C++.";
}
};
int main() {
Student stu;
stu.m_Name = "ZHH";
stu.learn(); // 输出:ZHH is learning C++.
return 0;
}
继承方式
- 公共继承:继承的公共成员和保护成员权限不变
- 保护继承:继承的公共成员变为保护权限
- 私有继承:继承的公共成员和保护成员变为私有权限
TIP
无论哪种继承方式,父类中的私有成员都不能被子类访问。
继承后对象的大小
父类的所有非静态成员属性都会被子类继承下去。尽管父类的私有属性,子类无法去访问,但是依旧被子类继承了。
C++
class Base {
public: int m_A;
protected: int m_B;
private: int m_C;
};
class Super :public Base {
public: int m_D;
};
int main() {
cout << sizeof(Super); // 输出:16,子类中包含父类的私有属性,共4个int
return 0;
}
也可以通过查看类的布局验证一下,在开发者命令行窗口输出:cl /d1 reportSingleClassLayout类名 文件名
。
shell
cl /d1 reportSingleClassLayoutSuper main.cpp
# 输出:
# class Super size(16):
# +---
# 0 | +--- (base class Base)
# 0 | | m_A
# 4 | | m_B
# 8 | | m_C
# | +---
# 12 | m_D
可以看到父类的私有属性 m_C
的确出现在了子类里面,虽然它无法被子类访问。
继承后的构造顺序
C++
class Base {
public:
Base() { cout << " Base()" << endl; }
~Base() { cout << "~Base()" << endl; }
};
class Super :public Base {
public:
Super() { cout << " Super()" << endl; }
~Super() { cout << "~Super()" << endl; }
};
int main() {
Super();
return 0;
}
// 输出:
// Base()
// Super()
// ~Super()
// ~Base()
构造顺序:先父后子
析构顺序:先子后父
访问继承的同名成员
- 访问自己的同名成员,直接访问即可。
- 访问父类的同名成员,需要加作用域。
C++
class Base {
public: void func() { cout << "Base" << endl; }
};
class Super :public Base {
public: void func() { cout << "Super" << endl; }
};
int main() {
Super s;
s.func();
s.Base::func(); // 访问父类的同名成员,需要加作用域
return 0;
}
访问同名静态成员也类似:
C++
class Base {
public: static int m_A;
};
class Super :public Base {
public: static int m_A;
};
int Base::m_A = 10;
int Super::m_A = 20;
int main() {
Super s;
// 通过对象访问
cout << s.m_A << endl;
cout << s.Base::m_A << endl; // 访问父类加作用域
// 通过类名访问
cout << Super::m_A << endl;
cout << Super::Base::m_A << endl; // 访问父类加作用域
return 0;
}
多继承
C++允许一个类继承多个类。
如果两个父类中有同名成员,子类将无法直接访问继承的该成员,也需要添加作用域。
C++
class Base1 {
public:
int m_A;
};
class Base2 {
public:
int m_A;
};
class Super :public Base1, public Base2 {};
int main() {
Super s;
s.m_A = 10; // 报错:提示:对m_A的访问不明确
s.Base2::m_A = 10; // 需要指定是哪个父类的作用域
return 0;
}
多态
多态分为两类:
- 静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定——编译阶段确定函数地址
- 动态多态的函数地址晚绑定——运行阶段确定函数地址
C++
class Person {
public:
void speak() {
cout << "Person" << endl;
}
// 关键字virtual声明虚函数,使编译器在编译时无法确定函数调用
virtual void run() {
cout << "Person" << endl;
}
};
class Student :public Person {
public:
void speak() { cout << "Student" << endl; }
virtual void run() { cout << "Student" << endl; }
};
// 静态多态,地址早绑定
void doSpeak(Person& p) {
p.speak();
}
// 动态多态,地址晚绑定
void doRun(Person& p) {
p.run();
}
int main() {
Student s;
doSpeak(s); // 输出:Person
doRun(s); // 输出:Student
return 0;
}
案例-计算器类
分别用普通写法和多态实现计算器类,可以从中发现多态的优点。
普通写法:
C++
class Calculator {
public:
int m_Num1; // 操作数1
int m_Num2; // 操作数2
int getResult(char oper) {
switch (oper) {
case '+':
return m_Num1 + m_Num2;
case '-':
return m_Num1 - m_Num2;
case '*':
return m_Num1 * m_Num2;
}
}
};
int main() {
Calculator c;
c.m_Num1 = 20;
c.m_Num2 = 10;
cout << c.getResult('+') << endl;
cout << c.getResult('-') << endl;
cout << c.getResult('*') << endl;
return 0;
}
多态写法:
C++
// 实现计算机抽象类
class AbstractCalculator {
public:
int m_Num1; // 操作数1
int m_Num2; // 操作数2
virtual int getResult() {
return 0;
};
};
// 加法计算器类
class AddCalculator :public AbstractCalculator {
public:
int getResult() {
return m_Num1 + m_Num2;
}
};
// 减法计算器类
class SubCalculator :public AbstractCalculator {
public:
int getResult() {
return m_Num1 - m_Num2;
}
};
// 乘法计算器类
class MulCalculator :public AbstractCalculator {
public:
int getResult() {
return m_Num1 * m_Num2;
}
};
int main() {
AbstractCalculator* abc = NULL;
abc = new AddCalculator;
abc->m_Num1 = 20;
abc->m_Num2 = 10;
cout << abc->getResult() << endl;
delete abc;
abc = new SubCalculator;
abc->m_Num1 = 20;
abc->m_Num2 = 10;
cout << abc->getResult() << endl;
delete abc;
abc = new MulCalculator;
abc->m_Num1 = 20;
abc->m_Num2 = 10;
cout << abc->getResult() << endl;
delete abc;
return 0;
}
对比可知,多态的优点:
- 代码组织结构清晰
- 可读性强
- 易于维护和扩展
纯虚函数和抽象类
在多态中,通常父类的虚函数的实现是没有意义的,主要都是调用子类重写的函数。
因此可以将虚函数改为 纯虚函数。
纯虚函数语法:virtual 数据类型 函数名 (参数列表) = 0
当类中有了纯虚函数,这个类也称为 抽象类。
抽象类的特点:
- 无法实例化对象
- 子类必须重写纯虚函数,否则也属于抽象类
C++
class Base {
public:
virtual int func() = 0;
};
class Super :public Base {
public:
int func() {} // 实现父类的纯虚函数
};
虚析构与纯虚析构
父类指针指向子类对象,当释放父类指针时,子类对象的析构函数不会被调用。
C++
class Base {
public:
Base() {
cout << "Base" << endl;
}
~Base() {
cout << "~Base" << endl;
}
};
class Super :public Base {
public:
Super() {
cout << "Super" << endl;
}
~Super() {
cout << "~Super" << endl;
}
};
int main() {
Base* b = new Super();
delete b;
return 0;
}
// 输出:
// Base
// Super
// ~Base
从上面的例子可以发现,子类的析构函数未被调用。解决办法是将父类的析构改为虚析构或纯虚析构。
文件操作
文本文件-写文件
写文件步骤如下:
- 包含头文件:
#include<fstream>
- 创建流对象:
ofstream ofs;
- 打开文件:
ofs.open(filename, mode);
- 写入数据:
ofs << data;
- 关闭文件:
ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件(append) |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,则使用 |
操作符。
例如: 用二进制方式写文件:ios::binary | ios::out
C++
#include<iostream>
#include<fstream> // 1. 包含头文件
using namespace std;
int main() {
ofstream ofs; // 2. 创建流对象
ofs.open("test.txt", ios::out); // 3. 打开文件
ofs << "Hello World!"; // 4. 写数据
ofs.close(); // 5. 关闭文件
return 0;
}
TIP
第二三步可合并为 ofstream ofs("test.txt", ios::out);
。
文本文件-读文件
读文件步骤如下:
- 包含头文件:
#include<fstream>
- 创建流对象:
ifstream ifs;
- 打开文件:
ifs.open(filename, mode);
- 读取数据:多种读取方式
- 关闭文件:
ifs.close();
TIP
打开文件后,可以通过 ifs.is_open()
确定是否打开。
C++
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main() {
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open()) return -1; // 文件打开失败
// 方法一:通过流对象的成员方法getline()
// char buffer[1024] = { 0 };
// while (ifs.getline(buffer, sizeof(buffer))) cout << buffer << endl;
// 方法二:通过全局函数getline(),需要导入<string>
// string buffer;
// while (getline(ifs, buffer)) cout << buffer << endl;
// 方法三:每次只读一个字符
char c;
while ((c = ifs.get()) != EOF) cout << c; // EOF: end of file
ifs.close();
return 0;
}
二进制文件-写文件
二进制文件写入要利用流对象的成员函数 write
函数原型:ostream& write(const char* buffer, int size)
C++
class Person {
public:
string name;
int age;
};
int main() {
Person p = { "ZHH", 22 };
ofstream ofs("test", ios::binary | ios::out);
ofs.write((const char*)&p, sizeof(Person));
ofs.close();
return 0;
}
二进制文件-读文件
二进制文件写入要利用流对象的成员函数 read
函数原型:istream& read(char* buffer, int size)
将上面刚刚存进去的二进制数据读取出来:
C++
class Person {
public:
string name;
int age;
};
int main() {
Person p;
ifstream ifs("test", ios::binary | ios::in);
ifs.read((char*)&p, sizeof(Person));
cout << p.name << endl;
ifs.close();
return 0;
}