高级程序设计课程笔记
高程的一些课程笔记
课程ppt: 高级程序设计2025春季
这里笔记的顺序是按照单元顺序来写的,只有大纲;
标注的必考和200%必考的更加注意。
抽象与封装
抽象与封装使得开发者无需关心底层
内部实现不影响外部的使用
例子:用链表封装stack
#include <bits/stdc++.h>
using namespace std;
typedef struct Node
{
int num;
Node *prev;
Node(int num, Node *prev = nullptr) : num(num), prev(prev) {}
} node;
class Stack
{
public:
void push(int num);
void pop();
int get_size();
bool empty();
int& top();
Stack();
// 在pubulic 里面定义的是interface
private:
node *tail;
int size = 0;
// 数据常常标为private
};
void Stack::push(int num)
{
size += 1;
tail = new node(num, tail);
}
void Stack::pop()
{
if (!empty())
{
delete tail;
tail = tail->prev;
size -= 1;
}
}
int Stack::get_size()
{
return size;
}
bool Stack::empty()
{
return size == 0;
}
int& Stack::top(){
return (tail->num);
}
Stack::Stack():tail(nullptr),size(0){}
int main()
{
Stack st;
for(int i=0;i<10;i++){
st.push(i);
}
for(int i=0;i<10;i++){
cout<<st.top()<<" ";
st.pop();
}
cout<<"\n";
for(int i=0;i<10;i++){
st.push(i);
}
for(int i=0;i<10;i++){
st.top()+=1;
cout<<st.top()<<" ";
st.pop();
}
cout<<"\n-------------------\n";
cout<<"size: "<<st.get_size()<<" isEmpty? "<<st.empty();
return 0;
}
面向对象
细节:class在定义完成之前是不能创建实例的;但是可以创建这个类型的pointer和reference
class A{
public:
A a;//Error! Compiler don't konw how much memory to allocate
A* p;//OK
A& r;//OK
}
下面内容必考,前面不是重点
对象的创建方式(注意不同创建的方式,高频考点)
直接创建 : A a; 内存在stack区,作用域结束后内存自动释放
间接方式创建动态变量:A* p=new A;
//在最后记得要delete p!!delete对应的是p所指的heap上面的空间 //p是栈上的变量,但是p所指的是heap上的一段空间 delete p;
public, private,protected:
- public: 可以在class之外被访问
- private: 在class之外不能访问,不能被derived class继承
- protected:在class之外不能别访问,可以被derived class继承
友元函数可以访问public和protected
对象作为函数参数进行传递,作为函数返回值(必考点,区分下面的区别)!!!必考
void fun(A a){
//...
}
//传入对象,并创建一个临时的A对象
void g(A& a){
//....
}
//传入的是原先对象的引用
Date f1(A& a){
return a;
}
//会创建一个临时变量并返回
Date& f1(A& a){
return a;
}
//只会return 临时的变量!
注意f1
return了原来的对象!!
构造函数和析构函数
构造函数可以有多个,可以重载:
class A{ public: A(); A(int a); A(A& a); } A::A(){ cout<<"Default class constructor\n"; } A::A(int a){ cout<<"This is " }
此外,还可以本地初始化,在类创建的时候,还可以
如果在instance中申请了额外的内存空间,那么必须自定义一个析构函数来销毁这个类的instance
什么时候必须要析构函数下面这个200%必考!!!!!!!!!!!

图中讲str赋值为nullptr是为了更加地安全。也可以没有
内存是怎么分配的?
- stack里面有一个String instance: int 4bytes, char* 4bytes; 8bytes in total;
- 在调用构造函数的时候,在heap上要了5个char位置,存放'a','b','c','d','\n'.
当这个类的instance的作用域消失以后,编译器会自动调用这个类的析构函数;如果没有的话,就会自动创造一个析构函数,但是这个默认的够细函数是不会自己去销毁heap上的内存,需要手动销毁,防止内存泄露!
this指针
每一个类的成员函数其实都会隐藏一个默认的this
指针。面向对象其实是一种思想。
不重要,不是考点。
拷贝构造函数(考点之一)
你必须care拷贝构造函数。
相当于是构造函数一种特殊的重载。
class A{
public:
int data;
A(int data):data(data)
A(A& a):data(a.data)//拷贝构造函数
}
三种情况下会调用这个拷贝构造函数:
- 利用一个instance 创造另一个instance
- 当一个instance作为参数传递给一个函数时(pass by value,会在函数内部调用拷贝构造函数创建一个新的临时对象)
- 当一个instance作为函数返回值时 (注意,和上面一样,不是referece,而是pass by value)
如果没有构造函数,就会生成一段简单的拷贝构造函数。
- 直接将成员里面的变量赋值给被拷贝的对象,是
shallow copy
!
比如,以下面这个200%会考的内容为例:
//例如,在下面的类中没有自定义拷贝构造函数:
class String
{ int len;
char *str;
public:
String(char *s)
{ len = strlen(s);
str = new char[len+1];
strcpy(str,s);
}
~String() { delete []str; len=0; str=NULL; }
};
......
String s1("abcd");
String s2(s1);
- 系统提供的隐式拷贝构造函数将会使得s1和s2的成员指针str指向同一块内存区域!
会带来一系列的内存安全问题
- 如果对一个对象(s1或s2)操作之后修改了这块空间的内容,则另一个对象将会受到影响。如果不是设计者特意所为,这将是一个隐藏的错误。
- 当对象s1和s2消亡时,将会分别去调用它们的析构函数,这会使得同一块内存区域将被归还两次,从而导致程序运行错误。
- 当对象s1和s2中有一个消亡,另一个还没消亡时,则会出现使用已被归还的空间问题!
我们需要自定义拷贝构造函数,进行deep copy
String(String& s){
len=s.len;
str=new char[len+1];
strcpy(str,s.str);//将s.str的内容复制到str中
}
3.2常成员函数
常成员函数
- 可以用const约束函数,使得这个函数不能修改类成员的值:
class A{
int a;
int* b;
public:
void func() const{
a=1;//Error, 不能改;
b=new int(1);//Error不能修改b的值
*b=1;//正确,因为b的值没有改变,只是改变了b所指的地址的值。
}
}
- 常成员只能调用常成员函数

静态数据成员
- 存储在静态存储区,内存为所有类的实例共享。
- 必须在类的内部声明,并且在类的外部初始化:
class A{
public:
static int a;//不能在类的内部赋值
}
\\.....
int A::a=10;//在类的外面赋值
静态成员函数
只能访问到静态成员对象
3.3友元
友元不是类的成员,但是可以访问类的所有成员,包括private和protected。
如:
熟悉下面这个即可
class A{
int a;
public:
friend ostream& operator<<(ostream& os, A& a){
os<<"A("<<a<<")";
return os;
}
friend istream& operator>>(istream& os,A& a){
os>>a.a;
return os;
}
};
3.4类的模块化设计
cpp里面的模块化设计
- 接口:包含被外界使用的类型定义、常量定义以及全局变量和函数的声明。(.h文件)
- 实现:包含本模块中所有的类型、常量、全局变量和函数的定义。(.cpp文件)
4.1Inheritance继承
派生类(derived class),即子类。
基类(base class),即父类。
继承从基类的数量,分为多继承(很少考)
和单继承
继承按照继承的方式可以分为 public继承
private继承(by default)
protected继承
派生类:
- 有子类的所有成员和函数,但是不能访问
private
成员;有,但是不能访问! - 可以override base class的成员
#include<iostream>
using namespace std;
class A{
int a;
public:
int fun(){
cout<<"驿站寄旧衣,七毛一公斤"<<endl;
}
};
class B:public A{
void fun(){
cout<<a;//Error
}
};
class C: public A{
int a;//override
void fun(){
cout<<a;//It's ok!
}
};
- 注意,如果要覆盖的话,就需要用作用域修饰符:
#include<iostream>
using namespace std;
class A{
int a;
public:
void fun(){
cout<<"sdddsds";
}
};
class B:public A{
public:
void fun(){
cout<<"I am a member function of B"<<endl;
}
void f(){
fun();
A::fun();
//output: I am a member function of B
// sdddsds
}
};
派生类的成员对外访问情况 | 百分之一百的考点!!!
考法为程序分析题,给一段代码,找出错误。
从base class继承而来的成员的访问限定情况,和继承的方式有关:

关注ppt上的那个例题。
子类型
派生类就是基类的一个子类型;
- 派生类对象可以直接赋值给基类,其中不属于基类的成员将被忽略。
- 基类的指针可以指向派生类。
drived class对象的创建和消亡
从基类集成的数据由基类的构造函数初始化,派生类的数据类型由派生类的构造函数初始化;
- 先执行基类的构造函数,再执行派生类的构造函数
- 默认是执行基类的默认构造函数,除非在派生类的构造函数的成员初始化列表中给出
class B: public A{
public:
B(): A(int a){
//初始化列表里面调用A的构造函数
}
}
//这样的话,B的默认构造函数会调用A的一个构造函数( A(int i){};)
如果派生类里面没有提供构造函数,则会生成一个derived class的默认构造函数,这个函数会自动调用基类的默认构造函数;
继承构造函数:允许派生类通过简单的声明来继承基类的构造函数
class B: public A{
using A::A;//继承了A的所有构造函数
//如果想要给派生类的成员赋值,那就需要自己写
B(int a,int b): A(a){
//在成员的初始化列表里面初始化A
//....这里可以给B的成员赋值
}
}
拷贝构造函数:如果不写,就会自动调用基类的拷贝构造函数;但是如果在子类的拷贝构造函数中需要调用base的拷贝构造函数,那么应该显式地在成员初始化列表中调用,否则调用的是base的默认构造函数
...
B(const B& b): A(b){}
...
等号的操作符重载:需要显式地调用base的赋值拷贝构造函数
class A { ...... };
class B: public A
{ ......
public:
B& operator =(const B& b)
{ if (&b == this) return *this; //防止自身赋值。
*(A*)this = b; //调用基类的赋值操作符对基类成员
//进行赋值。也可写成:
//this->A::operator =(b);
...... //对派生类的成员赋值
return *this;
}
};
派生类的构析函数
老师说:期末会参考这个例子:“继承的实例:一个公司中的职员类和部门经理类的设计。”(PPT 42)
可能考点:为什么要用 指针数组;
- 修改了这个对象之后其他的拷贝不会修改,但是用指针的话都是访问同一个对象
- 减少内存的开销,提升性能
一个例子
class Employee //普通职员类
{ String name; //String为字符串类。
int salary;
public:
Employee(const char *s, int n=0):name(s)
{ salary = n;
}
void set_salary(int n) { salary = n; }
int get_salary() const { return salary; }
......
};
const int MAX_NUM_OF_EMPS=20;
class Manager: public Employee //部门经理类
{ Employee *group[MAX_NUM_OF_EMPS];
int num_of_emps;
public:
Manager(const char *s, int n=0): Employee(s,n)
{ num_of_emps = 0;
}
bool add_employee(Employee *e);
bool remove_employee(Employee *e);
int get_num_of_emps() { return num_of_emps; }
......
};
//创建职员对象Jack和Jane
Employee e1("Jack",1000),e2("Jane",2000);
//创建经理对象Mark
Manager m("Mark",4000);
//把职员Jack和Jane纳入经理Mark的管理m.add_employee(&e1);
m.add_employee(&e2);
//老师上课说: 这里不一定是要用new在堆上来创建一个带地址的对象,在stack上创建之后完全可以将栈上的地址作为指针传入;
//显示经理Mark的工资
cout << "Mark's salary is " << m.get_salary() << '.'
<< endl;
//显示经理Mark的管理人数
cout<< "Number of employees managed by Mark is "
<< m.get_num_of_emps() << '.' << endl;
//职员Jack脱离经理Mark的管理
m.remove_employee(&e1);
......
老师上课说:可能出一个没有错误的纠错题,找出错误之后扣分。 -- 2025.3.12 10.45
4.2 多继承
多继承:同时继承多个成员
老师:考试不考,平时也不建议你用
4.3 聚合、组合
会有一到两个程序分析题考这个,主要要区分哪个是组合哪个是聚合
继承不是唯一代码复用的方式;继承体现的是:"is a type of",但是还有一种“a part of"的关系;
聚合:在聚合关系中,被包含的对象与包含它的对象独立创建和消亡,被包含的对象可以脱离包含它的对象独立存在。聚是一团火,散是满天星
class A { ...... };
class B //B与A是聚合关系
{ A *pm; //指向成员对象
public:
B(A *p) { pm = p; } //成员对象在聚合类对象外部创建,然后传入
~B() { pm = NULL; } //传进来的成员对象不再是聚合类对象的成员
......
};
......
A *pa=new A; //创建一个A类对象
B *pb=new B(pa); //创建一个聚合类对象,其成员对象是pa指向的对象
......
delete pb; //聚合类对象消亡了,其成员对象并没有消亡
...... // pa指向的对象还可以用在其它地方
delete pa; //聚合类对象原来的成员对象消亡
在这里A和B就是聚合关系,B的对象消亡的时候并不会导致B对象里面的A对象消亡,因为在B的析构函数里面,只是将对象A的指针赋值为NULL
,但是并没有delete对象A;
组合 | Composition:在组合关系中,被包含的对象随包含它的对象创建和消亡,被包含的对象不能脱离包含它的对象独立存在。
实现组合的方法:
- 直接在内部创建成员对象;
- 如果用指针的话:记得在组合类的构析函数中delete成员对象;
继承更容易实现子类型、多态,组合和聚合是无法实现的
5.1 虚函数和消息的动态绑定
静态绑定
如果一个对象有两个同名的函数(一个是从base class继承而来),一般情况下,会在编译时刻根据对象的类型来决定采用那个消息处理函数(静态绑定)
静态绑定调用哪个函数 要看调用点的这个形参的类型!
在编译的时候就已经确定了,调用那个函数由形参决定:
这个代码是必考的内容!!!!
class A
{ public:
void f();
};
class B: public A
{ public:
void f();
void g();
};
......
A a;
B b;
a.f(); //A的f
b.f(); //B的f
b.A::f(); //A的f
void main(){
A a;
func1(a);
func2(&a);
B b;
func1(b);
func2(&b);
}
//无论传入的是A还是B,都是调用A的f,因为这里的形参已经确定
void func1(A& x)
{ ......
x.f(); //调用A::f还是B::f ?
......
}
void func2(A *p)
{ ......
p->f(); //调用A::f还是B::f ?
......
}
消息的动态绑定 | Dynamic Dispatch
我们通常希望,在代码中能够根据对象的类型来执行对应的函数。
这个时候我们需要引入virtual function
在base class中,将某个函数声明为虚函数,则在调用这个函数的时候,就能根据对应的对象来调用对应的函数
class A
{ int x,y;
public:
virtual void f();
};
class B: public A
{ int z;
public:
void f();
void g();
};
void func1(A& x)
{ ......
x.f(); //调用A::f或B::f
......
}
void func2(A *p)
{ ......
p->f(); //调用A::f或B::f
......
}
......
A a;
func1(a); //在func1中调用A::f
func2(&a); //在func2中调用A::f
B b;
func1(b); //在func1中调用B::f
func2(&b); //在func2中调用B::f
Warning:
静态成员函数不能是虚函数
派生类不要写
virtual
**只有通过指针或者引用来调用函数,才能
dynamic bonding
!!!!!**必考内容!构造函数不能是虚函数,析构函数往往是虚函数
- 如果构造函数也动态绑定,老师说,会导致baseclass的成员初始化不完全 -- 存疑
- 析构函数如果不用虚函数的话,当删除基类指针的时候,就只会进行静态绑定,调用基类的析构函数
如果基类的析构函数不是虚函数,那么在删除基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数。
在构造函数和析构函数里面如果调用了虚函数,不进行动态绑定
reason:
- 因为先调用的是基类构造函数,这个时候derived class的成员还没初始化,如果就调用derived class的函数会出错
- 因为先调用的是derived class的析构函数,已经删除了派生类的成员对象,会出错。
class A { ...... public: A() { f(); } ~A() { f(); } virtual void f(); void g(); void h() { f(); g(); } }; class B: public A { ....... public: B() { ...... } ~B(); void f(); void g(); }; ...... A a; //调用A::A()和A::f a.f(); //调用A::f,f是虚函数,但是因为没有通过指针或引用调用,是静态绑定 a.g(); //调用A::g,静态绑定 a.h(); //调用A::h、A::f和A::g,这里调用A::f是动态绑定,因为是隐式地用this指针调用,f也是虚函数; //a消亡时会调用A::~A()和A::f B b; //调用B::B()、A::A()和A::f b.f(); //调用B::f b.g(); //调用B::g b.h(); //调用A::h、B::f和A::g 在这里调用f函数的时候是`dynamic bonding`;但是调用g的时候,因为这不是一个虚函数,所以只能是静态绑定 //b消亡时会调用B::~B()、A::~A()和A::f
check the type of bonding in each case above ! ! ! ! ! ! ! ! ! !
虚函数动态绑定的实现: 背后维护了一个vtable
;
纯虚函数和抽象类
将虚函数声明为纯虚函数,此时这个类变成了一个抽象类,不能创建对象,仅做为接口;
纯虚函数必须在derived class里面重写;
抽象类可以真正地实现继承和封装.
可能考 :
例如:
//A.h (类A的对外接口)
class A
{ int i,j;
public:
A();
A(int x,int y);
void f(int x);
};
//A.cpp (类A的实现,不公开)
#include "A.h"
void A::A() { ...... }
void A::A(int x,int y) { ...... }
void A::f(int x) { ...... }
......
//B.cpp (A类对象的某个使用者)
#include "A.h"
void func(A *p) //绕过对象类的访问控制!
{ p->f(2); //Ok
p->i = 1; //Error
p->j = 2; //Error
*((int *)p) = 1; //Ok,访问p所指向的对象的成员i
*((int *)p+1) = 2; //Ok,访问p所指向的对象的成员j
}
这样子打破了封装的效果!!!!
如何防止上面的情况?
只给一个抽象接口,让使用者只能看到抽象的接口,而看不到别的数据;
用抽象类I_A给类A提供一个抽象接口
//A.cpp (类A的实现,不公开)
#include "I_A.h"
class A: public I_A
{ int i,j;
public:
A();
A(int x,int y);
void f(int x);
};
void A::A() { ...... }
void A::A(int x,int y) { ...... }
void A::f(int x) { ...... }
......
//I_A.h (类A的对外接口)
class I_A
{ public:
virtual void f(int)=0;
};
//B.cpp (A类对象的某个使用者)
#include "I_A.h"
void func(I_A *p)
{ p->f(2); //Ok
*((int *)p) = 1; //这里不知道p所指向的对象有哪些数据成员,
//用户不知道数据定义情况,并不知道这样访问的是哪个数据成员
}
6 操作符重载
操作符重载可通过下面两个途径来实现:
•作为一个类的非静态的成员函数(操作符new和delete除外)。
•作为一个全局(友元)函数。
至少应该有一个参数是类、结构、枚举或它们的引用类型。
不管是采用成员函数还是全局函数,重载的函数名字都为:
operator #
“#”代表任意可重载的操作符。
操作符重载的原则:
只能重载C++语言中已有的操作符,不可臆造新的操作符。
可以重载C++中除下列操作符外的所有操作符:
“. ”, “.* ”,“?: ”,“:: ”,“sizeof ”
遵循已有操作符的语法:
•不能改变操作数个数。
•不改变原操作符的优先级和结合性。
尽量遵循已有操作符原来的语义:
•语言本身没有对此做任何规定,使用者自己把握 !
单元操作符
- 自增自减操作符
注意自增自减操作符有 前和后的区别
x++的值是x,++x的值是x+1; 且,x++得到的是一个right-value expression,但是++x是left-value expression
也就是说: ++(x++)或者 (x++)++都会报错,因为x++得到的是左值表达式
class Complex{
//...
Complex& operator++(){
//定义前增操作符
real++;
imag++;
return *this;
}
Complex operator++(int){
//定义后增操作符
Complex p=*this; //记得 1 。
real++;
imag++;
return p;
}
}
下面这个例子很重要,曾经考过:这个是必考内容!!!!!!!!(前++和后++的操作符重载!)
class Counter
{ int value;
public:
Counter() { value = 0; }
Counter& operator ++() //前置的++重载函数
{ value++;
return *this;
}
const Counter operator ++(int) //后置的++重载函数
{ Counter temp=*this; //保存原来的对象
value++; //写成:++(*this);更好!调用前置的++重载函数
return temp; //返回原来的对象
//temp的生存期仅在这个函数内,而会赋值temp给一个临时的对象。
}
};
Check your understanding:
Counter a,b,c;
++a; //使用的是不带参数的操作符++重载函数
a++; //使用的是带int型参数的操作符++重载函数
b = ++a; //加一之后的a赋值给b
c = a++; //加一之前的a赋值给c
++(++a);或 (++a)++; //OK,a加2
++(a++);或 (a++)++; //Error,编译不通过
请问c的类型是const吗?不是,仍为普通的,是否为const是由创建时声明决定的!(注:const的对象实例只能调用声明为const的成员函数,成员不能被改变)
请问可以写成Complex operator+++()吗?会有什么影响?
ans: 只会影响到类似
(++a)++
的句子,因为这个只会加一次,后面的后置++其实是对一个临时对象的++;请问后置++那个改成const Counter& operator++(int) 可以吗?
不可以!因为temp是临时的对象,不能return它的&,会出现
悬浮指针
问题,实际上编译器也会报错
双元操作符
特殊的操作符重载
赋值操作符 | 必考!!!
如果没有自定义赋值操作符,会自动生成有一个shallow copy的赋值函数;
但是如果涉及到指针,通常要自定义一个deepcopy的函数,防止指向同一块区域,并且释放原先自己指向的内存区域,防止内存泄露。
比如经典的String类:
解决上面问题的办法是针对String类自己定义赋值操作符重载函数:
class String
{ ......
String& operator = (const String& s)
{
if (&s == this) return *this; //防止自身赋值:a=a
delete []str;
str = new char[s.len+1];
strcpy(str,s.str);
len = s.len;
return *this;
}
};
注: 下面这一部分是使用了ai进行整理,使得ppt上面的内容更加的清晰,实际使用的时候建议和ppt一起使用。
C++ 自定义 new
和 delete
操作符详解
1. operator new
重载基础
基本语法
operator new
必须作为静态成员函数重载(static
关键字可省略):
void* operator new(size_t size);
- 返回类型:必须是
void*
- 参数:
size_t size
表示对象所需空间大小size_t
是sizeof()
操作符返回的类型(通常为unsigned long
)
使用方式
class A {
public:
void* operator new(size_t size) {
void* p = malloc(size);
memset(p, 0, size); // 初始化内存为0
return p;
}
};
A* p = new A; // 调用重载的operator new
特点
- 自动计算类大小并作为
size
参数传递 - 可以初始化内存(即使类没有构造函数)
- 不一定要在堆上分配内存(后面会讲placement new)
2. Placement New(定位new)
允许在已分配的内存上构造对象:
class A {
int x, y;
public:
A(int i, int j) : x(i), y(j) {}
void* operator new(size_t size, void* p) {
return p; // 直接返回传入的指针
}
};
char buf[sizeof(A)]; // 预分配内存(栈上)
A* p = new (buf) A(1, 2); // 在buf上构造对象
p->~A(); // 必须显式调用析构函数!
关键点
- 不分配内存,只构造对象
- 必须手动调用析构函数
- 常用于内存池、特殊硬件地址操作
3. operator delete
重载
基本语法
void operator delete(void* p[, size_t size]);
- 必须是静态成员函数
- 第二个参数
size_t
是可选的 - 必须与
operator new
配对实现
示例
class A {
int x, y;
public:
void* operator new(size_t size) {
void* p = malloc(size);
memset(p, 0, size);
return p;
}
void operator delete(void* p) {
free(p);
}
};
A* ptr = new A;
delete ptr; // 调用重载的delete
4. 内存池实现(必考)
设计思路
- 第一次创建对象时申请一大块内存
- 将大内存分割为对象大小的块,用链表管理
- 从链表中分配/回收内存,避免频繁系统调用
完整实现
class A {
// 类原有成员
static A* p_free; // 自由链表头指针
A* next; // 链表指针
public:
static void* operator new(size_t size);
static void operator delete(void* p);
};
A* A::p_free = nullptr;
const int NUM = 32; // 每次申请的对象数量
void* A::operator new(size_t size) {
if (p_free == nullptr) { // 首次分配
// 申请NUM个对象的大内存块
p_free = static_cast<A*>(malloc(size * NUM));
// 初始化自由链表
for (int i = 0; i < NUM-1; ++i) {
p_free[i].next = &p_free[i+1];
}
p_free[NUM-1].next = nullptr;
}
// 从链表分配一个对象
A* p = p_free;
p_free = p_free->next;
memset(p, 0, size);
return p;
}
void A::operator delete(void* p) {
// 将内存块插回链表头部
static_cast<A*>(p)->next = p_free;
p_free = static_cast<A*>(p);
}
内存布局示例
初始状态:
[对象1] -> [对象2] -> ... -> [对象32] -> NULL
经过多次分配释放后可能变成:
[对象5] -> [对象2] -> [对象10] -> ...(非连续)
关键优势
- 避免内存碎片
- 分配/释放效率高(O(1)时间复杂度)
- 减少系统调用开销
5. 内存释放问题(必考)
问题描述
当程序不再需要内存池时,如何释放所有申请的大内存块?
解决方案
需要额外维护一个列表记录所有申请的大内存块:
class A {
static A* p_free;
static std::vector<void*> big_blocks; // 记录所有大内存块
A* next;
public:
static void* operator new(size_t size);
static void operator delete(void* p);
static void release_memory(); // 释放所有内存
};
void A::release_memory() {
for (void* block : big_blocks) {
free(block);
}
big_blocks.clear();
p_free = nullptr;
}
void* A::operator new(size_t size) {
if (p_free == nullptr) {
void* block = malloc(size * NUM);
big_blocks.push_back(block);
// 初始化链表...
}
// 分配逻辑...
}
6. 重载new/delete的意义
- 提高效率:避免频繁系统调用
- 避免碎片:固定大小分配减少碎片
- 特殊需求:
- 调试/统计内存使用
- 硬件特定地址分配
- 实时系统(确定性的分配时间)
7. 注意事项
- 必须成对实现new/delete
- placement new必须手动调用析构函数
- 多线程环境需要加锁保护
- 继承情况下的行为(派生类会使用基类的operator new)
- 不要轻易替换全局new/delete

自定义类型转换
通过带参数的构造函数
通过操作符重载
class A{ int a; operator int(){ return a; } } ... A a; int b=(int)a;//类型转换
歧义问题:
- 比如我如果重载了int和A的加法,在执行
int b=1+a;
时,应该是调用int和A重载的加法呢?还是调用a到int的重载呢? - 如果我有从A到int的转换,也有从int到A的转换,那么执行
a+b
的时候应该调用哪一个呢?
解决方法:
- 比如我如果重载了int和A的加法,在执行
1. 手动显示类型转换
2. 在函数前面加上`explict` 关键字,不允许隐式类型转换;`explict operator int(){...}`;
第七章: IO,Handle Exception and Debug
输入输出
Warning:OJ必考!!!!
IO模式有三种:
- 文件输入输出
- 面向控制台的输入输出
- 面向字符串常亮的输入输出

(图为c++里面文件相关库)
- The
istream
class itself does not have a public constructor that you can directly use. Instead, it is designed to be inherited by derived classes likeifstream
,istringstream
, and others.
istream
is the base class of ifstream
and istringstream
, so you can define a function whose parameter is istream
type to handle both ifstream
and istringstream
.
例子:
Pay attention to the cases of PPT form page20-page25, the OJ would have related problems. But the final exam won't.
cin和cout的格式化输入输出:
- flush
- endl
cin和cout是有缓冲区的,请查询相关内容
等等。
文件的读写有三步:1. 打开文件 2. 进行操作 3. 关闭文件
打开文件
ofstream out_file(<文件名> [,<打开方式>])
ofstream out_file;
out_file.open(<文件名> [,<打开方式>]);
打开方式有
ios::app
,ios::out
等
判断文件是否成功打开
- out_file.is_open()
- out_file.fail()
文件的读写
n以文本方式输出的文件要以文本方式输入;以二进制方式输出的文件要以二进制方式输入!
文件的关闭
n以文本方式读写的文件要以文本方式打开;以二进制方式读写的文件要以二进制方式打开!
异常处理
很多时候,程序没有语法错误,但是存在隐式的错误,比如 x/y在某个事件段出现 y=0
则会出现Division by zero
的错误。还有比如内存溢出的错误。
异常处理的方法:
本地处理
异地处理
一个case是PA1的FileSimulator,当输入的cmd有错误时,error不是就地处理,而是回到循环调用readCommand函数里,处理异常。
在c++中除了可以通过函数的返回值来判断函数执行是否正常(Liunx用c写的,出现了很多这样的逻辑) 还可以用c++的结构化处理异常的机制try catch
try: 把有可能遭遇异常的代码(语句或函数调用)构成一个try语句块。
throw: 如果try语句块中的某部分代码在执行中发生了异常,则由throw语句产生一个异常对象,之后的操作不再进行。
catch: 抛掷的异常对象通过catch语句块来捕获并处理之。
注意:不一定是throw一个字符串或者一个runtime_error,你可以throw anything, but 要在catch中捕获相同的代码
void fun(fstream* fs){
if(!fs->open()){
throw fs;
}
}
...
try{
fun(fs1);
}catch(fstream* failed_file){
...
}
一个try后可以接多个catch,用于捕获不同的异常
void f(){
try{
throw 1;
} catch(int a){
cout<<"Catch "<<a<<endl;
throw 2;
}
}
try{
f();
g();
}catch(int ){
...
}catch(char* ){
...
}
如果当前的throw
没有被catch
,那么会回退到上一层,看是否会被捕获,如果最终也没有被捕获,那么就会abort
并退出。
try catch的例子是期末必考的题目!!!! 必考 10分左右!!
看ppt的例子h(),g(),f()
异常处理机制
- 每一个函数都有一个catch表
- 执行throw的时候看当前catch表是否能够正常捕获
- 如果不行,那么退栈,回到上一个函数
- 如果能够被catch,那么从这个函数继续执行
- 否则,回到main函数执行
terminate()
to end the program.
基于断言的调试
assert(expr)
will throw an error when expr
is false otherwise noting happend.
- Easy to understand
- Debug
assert的实现:assert其实是一个宏,定义类似这样:
#define assert(exp) ((exp)?(void)0:<输出诊断语句 and call abort( to end the program)>))
事件驱动的程序设计
略,自己看ppt
第八章
泛型程序设计
一个程序实体能对多种类型的数据进行操作或描述的特性称为类属或泛型(Generics)。
具有类属特性的程序实体通常有:
类属函数:一个能对不同类型的数据完成相同操作的函数。
类属类:一个成员类型可变、但功能不变的类。
基于具有类属特性的程序实体进行程序设计的技术称为:泛型程序设计(Generic Programming)
类属函数
能对不同类型的参数实现相同的操作。
比如:
不排除考察的可能性,因为指针是c++的精华
//函数定义:
typedef unsigned char byte;
void sort(void *base, //需排序的数据(数组)内存首地址
unsigned int num, //数据元素的个数
unsigned int element_size, //一个数据元素所占内存大小(字节数)
bool (*cmp)(const void *, const void *) ) //比较两个元素的函数
{ //不论采用何种排序算法,一般都需要对数组进行以下操作:
//取第i个元素
(byte *)base+i*element_size
//比较第i个和第j个元素的大小 (利用调用者提供的回调函数cmp实现)
cmp((byte *)base+i*element_size,(byte *)base+j*element_size)
//交换第i个和第j个元素的位置
byte *p1=(byte *)base+i*element_size,
*p2=(byte *)base+j*element_size;
for (int k=0; k<element_size; k++)
{ byte temp=p1[k]; p1[k] = p2[k]; p2[k] = temp;
}
}
使用void*的指针来实现对不同的数据进行操作,很Amazing!!!
函数模板
函数模板是带有类型参数的函数定义
定义的模板如下:
template<class T>
void sort(T element[], unsigned int count){
//...
}
其中,template<class T> or template<typename T>
的含义是声明一个模板。
在调用函数的时候,可以显式的声明模板类型func<int>(a,b)
,这样的话强制设置模板为int,末班也可以设定一个默认值:template<typename T=int>
同时要注意,这里的template必须连着函数或者类的定义
template<typename T=int>
T add(T a,T b){
return a+b;
}
T sub(T a, T b){ //Error, T is undefined
return a-b;
}
类模板
如果一个类定义中用到的类型可变、但操作不变,则该类称为类属类。
在C++中,类属类用类模板实现。
类模板的格式为:
template <class T1,class T2,...> //class也可以写成typename
class <类名>
{ <类成员说明>
}
- T1、T2等为类模板的类型参数,在类成员的说明中可以用T1、T2等来作为它们的类型。
比如我们可以使用模板来实现适合多种类型的模板类:
例如,用类模板实现类属的栈类:
template <class T>
class Stack
{ T buffer[100];
int top;
public:
Stack() { top = -1; }
void push(const T &x);
void pop(T &x);
};
template <class T>
void Stack <T>::push(const T &x) { ...... }
template <class T>
void Stack <T>::pop(T &x) { ...... }
在创建类的实例时:
Stack<int> a; //显式地指出
在实现类的各个函数时:
template<typename T>
void Stack<T>::pop(T &X)
注意:不同模板的类成员不会共享相同的静态成员;即Stack<int>和 Stack<double>的静态成员相互独立。友元函数也是这样的!
因为只有实例化的时候才又实实在在的class生成!
使用一个模板之前首先要对其实例化(用一个具体的类型去替代模板的类型参数),实例化是在(预)编译时刻进行。
模板实例化一定要见到相应的源代码,否则无法实例化
一个例子:
// file1.h
template <class T>
class S //类模板S的定义
{ T a;
public:
void f();
};
extern void func();
// file1.cpp
#include "file1.h"
template <class T>
void S<T>::f() //类模板S中f的实现
{ ......
}
void func()
{ S<float> x; //实例化“S<float>”并创建该类的一个对象x
x.f(); //实例化“void S<float>::f()”并调用之
}
// file2.cpp
#include "file1.h"
int main()
{ S<float> s1; //实例化“S<float>”并创建该类的一个对象s1
s1.f(); //没有实例化“void S<float>::f()”,但调用之
S<int> s2; //实例化“S<int>”并创建该类的一个对象s2
s2.f(); //没有实例化“void S<int>::f()”,但调用之
func();
return 0;
}/*
两个模块都能通过编译,但在链接时出错,链接程序指出:
file2中调用的“void S<int>::f()”不存在!
S<float>::f() 没错的原因 :S<float>::f() 的实例化发生在 file1.cpp 中,因此链接器可以找到它的定义。
S<int>::f() 出错的原因 :S<int>::f() 的实例化未发生,因为其实现对 file2.cpp 不可见。
*/
在模板实例化的时候,编译器会根据相应的类型复制出相同逻辑,类型不同的代码。 也就是在做代码的拷贝和类型的替换!
代码替换的情况:
- 头文件的include
- 类的一些默认生成的函数
- 模板函数和模板类
- inline函数
- 宏定义
STL编程
STL由容器,算法,迭代器构成。
自己看ppt吧,内容还挺多的。
其他内容
并行程序设计
并发和并行:
- 并发: 同一时间段可以交替处理多个操作
- 并行:同一个事件可以同时进行多个操作
进程和线程:
- process 进程: 在一个内存空间上运行的应用程序。每一个进程都有多个线程。
- thread 线程: 进程中的一个执行任务。同类的多个线程共享进程的堆和方法区,但是也有自己的程序计数器和本地方法栈。