Skip to content

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 默认为私有权限。

构造函数类名() {}

  1. 无返回类型,无返回值
  2. 可以有参数,可重载
  3. 对象创建时自动调用

析构函数~类名() {}

  1. 无返回类型,无返回值
  2. 无参数,不可重载
  3. 对象销毁时自动调用
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

从上面的例子可以发现,子类的析构函数未被调用。解决办法是将父类的析构改为虚析构或纯虚析构。

文件操作

文本文件-写文件

写文件步骤如下:

  1. 包含头文件:#include<fstream>
  2. 创建流对象:ofstream ofs;
  3. 打开文件:ofs.open(filename, mode);
  4. 写入数据:ofs << data;
  5. 关闭文件: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);

文本文件-读文件

读文件步骤如下:

  1. 包含头文件:#include<fstream>
  2. 创建流对象:ifstream ifs;
  3. 打开文件:ifs.open(filename, mode);
  4. 读取数据:多种读取方式
  5. 关闭文件: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;
}