面向对象程序设计

一、构造函数

  1. 成员函数定义

  2. 初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Clock {
public:
/*初始化列表*/
Clock(int NewH, int NewM, int NewS):Hour(NewH),Minute(NewM),Second(NewS)
{

}
/*成员函数声明*/
void ShowTime();

private:
int Hour, Minute, Second;
};
/*成员函数定义*/
void Clock::ShowTime() {
cout << Hour << ":" << Minute << ":" << Second << endl;
}
  1. new/delete
1
2
3
4
5
 A *p = new A[3];
delete[] p;

A * pa;
pa = new A(3,5);
  1. 默认构造函数
1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
int value;
// 默认构造函数,没有参数
MyClass() : value(0) { // 初始化列表用于初始化成员变量
}
// 另一个构造函数,带有一个参数
MyClass(int v) : value(v) { // 初始化列表用于初始化成员变量
}
};
  1. 默认构造函数&缺省参数初始化

默认构造函数是指不带参数(MyClass() {})或所有参数都有默认值的构造函数(MyClass() : data(0) {} )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
class MyClass {
private:
int data;
public:
// 显示地写出 默认构造函数
MyClass() : data(0) {
std::cout << "Default Constructor called" << std::endl;
}
// 参数化构造函数
MyClass(int d) : data(d) {
std::cout << "Parameterized Constructor called" << std::endl;
}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用参数化构造函数
}

如果类中定义了参数化构造函数,而没有定义默认构造函数,则编译器不会自动生成默认构造函数。这意味着,如果你需要在没有提供参数的情况下创建对象,就必须显式地定义默认构造函数。

1
2
3
4
5
6
class MyClass {
public:
MyClass(int a = 0, int b = 0) {
// ...
}
};

这个例子中,你可以通过 MyClass obj; 来创建一个 MyClass 的对象,就像有一个默认构造函数一样。

缺省参数(也称为默认参数)是函数参数在声明时指定的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数声明,其中 b 和 c 是缺省参数
void func(int a, int b = 20, int c = 30);

int main() {
// 调用函数,只提供一个参数
func(10); // 输出:a = 10, b = 20, c = 30

// 调用函数,提供两个参数
func(10, 50); // 输出:a = 10, b = 50, c = 30

// 调用函数,提供三个参数
func(10, 50, 100); // 输出:a = 10, b = 50, c = 100

return 0;
}

从右向左:缺省参数必须从右向左依次声明,不能跳过中间的参数。

  1. 日期类
1
2
3
4
5
6
7
8
9
10
11
//获取某年某月的天数
int GetMonthDay(int year,int month)
{
static int days[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
if(month==2&&((year%4==0&&year%100!=0)||(year%400==0))){
return 29;
}
else{
return days[month];
}
}
  1. 拷贝构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
class MyClass {
private:
int data;
public:
// 构造函数
MyClass(int d) : data(d) {
std::cout << "Constructor called" << std::endl;
}
// 拷贝构造函数
MyClass(const MyClass &other) : data(other.data) {
std::cout << "Copy Constructor called" << std::endl;
}
// 成员函数
void display() {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
MyClass obj1(10); // 调用构造函数
MyClass obj2(obj1); // 调用拷贝构造函数
MyClass obj3 = obj1; // 通常调用拷贝构造函数
return 0;
}
  1. 构造函数的转换构造函数

当构造函数只有一个参数时,C++ 允许使用这个构造函数进行隐式类型转换。这种构造函数被称为转换构造函数。

例如,假设我们有一个类 A,它有一个接受 int 参数的构造函数:

1
2
3
4
5
6
class A {
public:
A(int i) {
// 初始化对象
}
};

在这种情况下,我们可以使用 int 来创建 A 类的对象,就像这样:

1
A a = 10;  // 隐式调用 A(int) 构造函数

二、内联函数inline()

使用 inline 关键字声明或定义的函数被称为内联函数。一般情况下编译器会用内联函数的代码直接替换函数调用(内联展开),这样就省去了函数调用时的跳转以及返回等操作,因此内联函数的运行速度比常规函数稍快,

一般情况下,在类定义中的定义的函数都是内联函数,而在类外定义是需要通过 inline 关键字显式指定。

1
2
3
inline int getMax(int a, int b) {
return a > b ? a : b;
}

三、引用

常引用

  1. 例子
1
2
3
4
5
wrong:
int & a = 10;/*非常量引用的初始值必须为左值*/

correct:
const int & a = 10;
  • int & a = 10; 这行代码声明了一个对整数类型的引用 a 并将其初始化为值 10。这里的 10 是一个右值(rvalue),指的是一个临时的、不可重复使用的值,而不是一个可以被赋值的持久对象(左值,lvalue)。

  • 这里的 const 关键字表明 a 是一个只读引用,它不能被用来修改它所引用的对象。

  1. 下例类比
1
2
3
4
5
6
7
int fun(){
return 6;
}
int main(){
const int & ret=fun();
return 0;
}

四、const修饰

  • const 修饰成员函数,该函数只能访问而不能修改成员变量,如果需要修改时,需要用 mutable 修饰该变量。
  • const 修饰成员变量

必须在构造函数的初始化列表中进行初始化,因为它们在对象创建后就不能再被修改。这就意味着,你不能在构造函数的主体中或者在其他任何地方修改常量成员变量的值。

1
2
3
4
5
6
7
8
class MyClass {
public:
const int myConst; // 常量成员变量

MyClass(int val) : myConst(val) { // 在构造函数的初始化列表中初始化常量成员变量
// myConst = val; // 错误:不能在构造函数的主体中修改常量成员变量的值
}
};
  • const 修饰对象,称之为常对象,只能调用 const 修饰的成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class MyClass {
    public:
    int value; // 默认为非const成员变量
    MyClass(int v) : value(v) {}
    // 普通成员函数,可以修改成员变量
    void setValue(int v) {
    value = v;
    }
    // const成员函数,不能修改非mutable成员变量
    int getValue() const {
    return value;
    }
    };

    int main() {
    const MyClass obj(10); // 创建一个const对象
    // 调用const成员函数
    int val = obj.getValue(); // 正确
    cout<<val;
    // 不能调用非const成员函数
    obj.setValue(20); // 错误
    return 0;
    }

非常量对象可以调用其常量成员函数和非常量成员函数。

然而,常量对象只能调用其常量成员函数,不能调用非常量成员函数。

const #define 的区别:

特性/关键字 const #define
处理阶段 编译、运行阶段 预处理阶段
类型检查 执行类型检查 不进行类型检查
调试支持 支持调试 无法调试
字符串替换 不是简单的字符串替换 简单的字符串替换
边界效应 不存在边界效应 存在边界效应
内存分配 可能需要内存分配 不需要内存分配

五、静态成员

  • 静态成员变量:静态成员变量存储在全局数据段。它们是类的所有对象共享的,即类的所有实例都会共享同一份静态成员变量。静态成员变量需要在类定义外部进行初始化。

  • 静态成员函数:静态成员函数存储在代码段。它们不属于类的任何一个对象,而是属于类本身。静态成员函数可以在没有类的对象的情况下被调用,只需要使用类名和作用域解析运算符 ::==没有this指针==

  • 静态数据成员被存放在==静态存储区==

静态数据成员所占的空间不会随着对象的产生而分配,也不会随着对象的消失而回收。只有在程序结束时才被系统释放。

  • 必须初始化
1
2
3
4
5
6
private:
int a,b,c;
static int s;
};
/*类外初始化静态成员*/
int MY::s=0;
  • 在静态成员函数中可以直接引用其静态成员,而引用非静态成员时需用对象名引用。

当在类外部使用 X::func() 语法调用某个函数时,这个函数 func 必须是类 X 的静态成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
class X {
public:
static void func() {
std::cout << "Static member function called" << std::endl;
}
};

int main() {
X::func(); // 正确,调用静态成员函数
return 0;
}


六、友元

6.1友元函数

MyClass 有一个私有成员 valueshowValue 函数被声明为 MyClass 的友元,这意味着它可以访问 MyClass 的私有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {
private:
int value;

public:
MyClass(int v) : value(v) {} // 构造函数

// 声明友元函数
friend void showValue(MyClass &c);
};

// 定义友元函数
void showValue(MyClass &c) {
std::cout << "The value of MyClass is: " << c.value << std::endl;
}

6.2友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public:
A() : a(0) { }
// B 是 A 的友元类,所以 B 可以访问 A 的私有和保护成员
friend class B;
private:
int a;
};

class B {
public:
void showA(A& x) {
// 直接访问 A 的私有成员
std::cout << "A::a=" << x.a << std::endl;
}
};

七、运算符重载

运算符重载可以通过成员函数重载或者友元函数重载实现。

  • 运算符 ::?:..*sizeoftypeidconst_castdynamic_castreinterpret_caststatic_cast 不允许重载

  • 双目运算符 =()[]-> 与类型转换运算符只能以成员函数方式重载,流运算符 <<>> 只能以友元函数的方式重载。

  • 将运算符函数定义为成员函数时,调用成员函数的对象(this指向的对象)被作为运算符的第一个操作数,所以如果是一元运算符,无需提供参数。

  • 如果将运算符函数定义为全局函数,则通常要将其声明为类的友元函数。

  • const Byte& operator+( ) const {}

    1. 第一个const:位于成员函数声明的左侧,它修饰的是返回类型。这里的const表示返回值是一个常量引用,即它不能被修改。
    2. 第二个const:位于成员函数声明的右侧,它修饰的是成员函数本身。这个const关键字表示这个函数是一个常量成员函数,它不会修改调用它的对象的状态。

7.1 成员函数重载一元运算符

1
2
3
4
5
6
7
8
9
Date&operator++()
{
cout<<"operator++"<<endl;
d++,m++,y++;
return *this;
}

Date d1(8,4,2024);
++d1;

Date&operator++()重载前置++

Date&operator++(int)重载后置++;

  • 此处的int没有意义。只是用于区分。系统调用时传0。

7.2 用友元函数重载一元运算符

1
2
friend const Integer&   operator++(Integer& a);   // 前缀++
friend const Integer operator++(Integer& a, int);//后缀++

全局函数时,第一个const的含义是返回值类型被修饰。

  • 前缀形式返回改变后的对象,返回*this
  • 后缀形式返回改变之前的值,所以必须创建一个代表这个值的独立对象并返回它,是通过传值方式返回的。

7.3 成员函数重载二元运算符

重载赋值运算符

1
2
3
4
5
Byte& operator=(const Byte& right) { // 只能用成员函数重载
if(this == &right) return *this; // 自赋值检测
b = right.b;
return *this;
}
1
2
3
4
5
6
7
8
9
MinInt operator+(const MinInt& rv) const {//二元运算 +
cout << "MinInt::operator+" << endl;
return MinInt(b + rv.b);
}
MinInt& operator+=(const MinInt& rv){//复合赋值运算 +=
cout << "MinInt::operator+=" << endl;
b += rv.b;
return *this;
}

使用成员运算符的限制是左操作数必须是当前类的对象。

7.4 全局函数重载二元运算符

成员函数重载左操作数的类型是固定的。例如重载了+,那么date+1可以,1+date不可以。==因为默认了左操作数的类型。但重载全局函数不存在该问题。==

希望运算符的两个操作数都能进行类型转换,则使用全局函数重载运算符。

1
2
3
4
重载operator>>的基本形式如下:
istream& operator>> (istream&, type&);
重载operator<<的基本形式如下:
ostream& operator<< (ostream&, const type&);
1
2
3
4
5
    friend ostream& operator<<(ostream&os,const Date&d);

ostream& operator<<(ostream&os,const Date&d){
return os<<d.y<<"~"<<d.m<<"~"<<d.d<<endl;
}

重要实例代码

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
#include <iostream>

class Data {
private:
int num;

public:
Data(int n) : num(n) {}

Data& operator+=(int n) {
num += n;
return *this;
}

friend Data operator+(int n, const Data& d);

void show() const {
std::cout << num << std::endl;
}
};

Data operator+(int n, const Data& d) {
return Data(d.num + n);
}

int main() {
Data d1(10);
d1.show();
d1 += 2;
d1.show();

Data d2 = 2 + d1;
d2.show();

return 0;
}

7.5返回值优化

  • 创建一个临时的Integer对象并返回它

Integer temp(left.i + right.i);return temp;

  • 临时对象语法

return Integer(left.i + right.i);

编译器直接把这个对象创建在外部返回值的存储单元中,所以只需要调用一次构造函数,不需要拷贝构造函数和析构函数的调用。因此,使用临时对象语法的效率非常高,这被称为返回值优化。


八、继承和派生

8.1访问权限

① 基类中的私有成员无论哪种继承方式在派生类 中都是不能直接访问的。

② 在公有继承方式下,基类中公有成员和保护成 员在派生类中仍然是公有成员和保护成员。

③ 在私有继承方式下,基类中公有成员和保护成 员在派生类中都为私有成员。

④ 在保护继承方式下,基类中公有成员和保护成 员在派生类中都为保护成员。

  • private成员:私有成员只能被定义它们的类的成员函数访问,其他任何地方都不能直接访问。这意味着私有成员对于类的外部是不可见的。

  • protected成员:保护成员也只能被定义它们的类的成员函数访问,但它们还可以被该类的派生类的成员函数访问。对于类的外部,保护成员和私有成员一样是不可见的。

8.2单继承和多继承

构造函数和析构函数不可以继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基类:圆
class Circle {
protected:
double radius;
public:
Circle(double r) : radius(r) {}
};

// 派生类:圆锥
class Cone : public Circle {
private:
double height;
public:
Cone(double r, double h) : Circle(r), height(h) {}
};

多继承的二义性

  1. 第一种二义性:调用不同基类中的相同成员时可能 出现二义性。

==解决方法:在函数调用时指明基类。==

例:

1
2
3
4
DateTime dateTime(2024, 4, 22, 14, 30, 59);
dateTime.Date::print(); // 调用日期类的打印函数
dateTime.Time::print(); // 调用时间类的打印函数
dateTime.print();
  1. 第二种二义性:菱形继承。

解决方法:虚基类。

  • 多重继承的继承函数书写格式
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
class A1{
protected:
int a1;
public:
A1(int a = 0):a1(a)
{
cout << "执行基类A1的构造函数" << endl;
}
};
class A2{
protected:
int a2;
public:
A2(int a = 0):a2(a)
{
cout << "执行基类A2的构造函数" << endl;
}
};

class B:private A2,public A1{
public:
B(int a1 = 0,int a2 = 0):A1(a1),A2(a2)
{
cout << "执行派生类B的构造函数" << endl;
}
};
  • 多重继承的同名函数调用
1
2
cout << setiosflags(ios::fixed) << setprecision(2) << s.GetArea()<< endl;//Sphere类的
cout << s.Circle::GetArea() << endl;//Circle类的

8.3虚基类

为最远的派生类提供唯一的基类成员,而不重复产生多 次拷贝。

1
2
3
4
class B{ private: int b;};
class B1 : virtual public B { private: int b1;};
class B2 : virtual public B { private: int b2;};
class C : public B1, public B2{ private: float d;}

派生类只能通过最底层的派生类访问虚基类的成员:在虚基类的派生中,派生类只能通过最底层的派生类来访问虚基类的成员,而不能通过其他派生类来访问。这是因为虚基类的成员只会被继承一次,而且只会在最底层的派生类中存在。

1
2
3
4
5
6
7
8
9
10
11
class Vehicle{
};

class Boat: virtual public Vehicle{
};

class Car: virtual public Vehicle{
};

class AmphibianCar:public Car,public Boat{
};

派生类的构造函数承担着对基类中数据成员初始化和对派生类自身数据成员初始化的双重任务

当创建一个派生类的对象时,它的构造函数会自动调用所有基类的默认构造函数(没有参数的构造函数)来初始化基类部分的数据。

1
2
3
4
5
6
class Teacher_Officer:public Teacher,public Officer{
public:
Teacher_Officer(string a,string b,string c):Staff(a), Teacher(a, b),Officer(a,c)
{
}
};

九、多态

多态性是指相同的动词作⽤到不同类型的对象上。

==C++实现的多态性:==

编译时(静态联编):

  • 函数重载
  • 运算符重载
  • 模板

运行时(动态联编):

  • 虚函数

9.1静态联编和动态联编

联编:⼀个源程序需要经过编译、连接,才能成为可执 ⾏代码。上述过程中需要将⼀个函数调⽤链接上 相应的函数代码,这⼀过程称为联编。

virtual 关键字的作⽤:指示C++编译 器对该函数的调⽤进⾏动态联编。

要使用动态联编,必须满足以下两个条件:

  1. 成员函数必须是虚函数(virtual function)。

  2. 必须通过指向基类的指针或引用来调用成员函数。

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
#include <iostream>
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};
class Circle : public Shape {
public:
void draw() {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() {
std::cout << "Drawing a rectangle." << std::endl;
}
};
int main() {
Circle c;
Rectangle r;
c.draw();
r.draw();

Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
shape1->draw(); // 调用Circle类的draw函数
shape2->draw(); // 调用Rectangle类的draw函数
delete shape1;
delete shape2;
return 0;
}
  • c.draw();r.draw(); 是通过对象直接调用成员函数,这种情况下,编译器在编译时期就可以确定调用哪个函数,这是静态联编。

  • 然而,shape1->draw();shape2->draw(); 是通过指向基类 Shape 的指针调用成员函数。这种情况下,编译器在编译时期无法确定 shape1shape2 指向的具体类型,只能在运行时根据 shape1shape2 实际指向的对象类型来确定调用哪个函数,这就是动态联编。

9.2 虚函数

访问对象的不同去调用不同的函数。只有当访问虚函数是通过基类指针s时才可获得运⾏时的多态性。

虚函数只能是成员函数

函数覆盖(override):在派⽣类中,虚函数被重新定义以实现不同的操作。 这种⽅式称为函数超越 (overriding),⼜称 为函数覆盖。

纯虚函数:纯虚函数是指在基类中声明但是没有定义的虚函数,⽽且设置函数值等于零。

1
virtual float area( )=0;

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

抽象类:抽象类:包含有纯虚函数的类称为抽象类。

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
class shape{
public:
virtual double area()=0;
};

class triangle:public shape{
private:
int s;
int h;
public:
triangle(int ss,int hh):s(ss),h(hh){}
double area()
{
double ret=s*h*0.5;
return ret;
}
};

class circle:public shape{
private:
int r;
public:
circle(int rr):r(rr){}
double area()
{
double ret=r*r*M_PI;
return ret;
}
};

int main()
{
/*通过指针调用被重新定义的虚函数,实现动态联编*/
shape*ptr;
circle c(1);
ptr=&c;
cout<<"the area of circle is "<<ptr->area()<<endl;
triangle t(2,3);
ptr=&t;
cout<<"the area of triangle is "<<ptr->area()<<endl;

/*函数隐藏*/
circle c(1);
cout<<"the area of circle is "<<c.area()<<endl;
triangle t(2,3);
cout<<"the area of triangle is "<<t.area()<<endl;
return 0;
}

9.3函数隐藏和函数重写

函数重写 (Function Overriding) 函数隐藏 (Function Hiding)
概念 派生类中新定义的函数版本覆盖基类中的同名虚函数,实现多态性。 派生类中定义与基类中同名的非虚函数,导致基类中的同名函数在派生类中不可见。
调用方式 根据指针或引用所指向的对象的实际类型来确定调用哪个版本的函数,实现运行时多态性。 当派生类定义了与基类同名的非虚函数时,调用该函数时将会调用派生类中的版本,而基类中的同名函数会被隐藏,无法直接调用。
关键字 基类中的函数必须声明为虚函数,并在派生类中使用 override 关键字进行重写。 派生类中的函数可以是虚函数,也可以是普通成员函数,不需要额外的关键字。
作用范围 发生在具有继承关系的类之间,通常用于实现基类与派生类之间的多态性。 同样也发生在具有继承关系的类之间,但是是通过在派生类中定义同名非虚函数而发生。

十、文件与输入输出控制

10.1 iostream中的控制符

1
2
cout<<hex<<num<<endl;
cout<<oct<<num<<endl;

10.2 iomanip中的控制符

  • setfill('x')setfill开始之后每个填充都会是'*',记得重置
  • setbase(16)设置输出的进制
  • setw(10)右对齐设置宽度为10,一次性,只可作用于一个输出
  • setiosflags(ios::left/right);设置左、右对齐
  • setprecision(n)设置输出数据的精确度为n,包括小数和整数部分。

分析:

1.

1
2
3
4
cout << setfill('*') << setw(10) << "Hello" << endl; // 输出"Hello"并用'*'填充到10个字符宽
cout << setbase(16)<<17<<endl;//设置输出进制
cout <<setiosflags(ios::left)<<names[i]<<resetiosflags(ios::left);
cout<<setw(15)<<setiosflags(ios::left)<<setiosflags(ios::right)<<"hello world!"<<"new node"<<endl;
  • setw(15)仅针对一个输入“hello world”后设置的右对齐会覆盖左对齐。

2.

1
2
3
double num=10.1;
cout<<setw(10)<<setprecision(5)<<num<<endl;
cout<<setw(10)<<fixed<<setprecision(5)<<num<<endl;
  • 仅用setprecision(5)不能精确控制小数位数,通常连用fixed<<setprecision(5)

10.3文件操作

  1. 文件访问方式
  • std::ofstream:用于文件输出。
  • std::ifstream:用于文件输入。
  • std::fstream:同时支持文件输入和输出。
1
2
using namespace std;
fstream f1=("test.txt");
  1. 文件打开
1
2
3
4
5
/*两种打开方式等价*/
fstream f1("hello.txt");

fstream f1; //定义文件输入流对象
f1.open("hello.txt"); //打开文件
  1. 文件关闭
1
f1.close();

关闭文件操作包括把缓冲区数据完整地写入文件,添加文件结束标志,切断流对象和外部文件的连接

  1. 文件的读出写入
1
2
3
4
f1<<20<<"hello"<<40;//在文件的开头插入

int a,b;//将文件开头的数据传入参数中,以空格分割
f1>>a>>b;
1
2
3
4
5
char c;
while(f1) { //判断文件未结束则循环
f1.get(c);//读取单个字符
f2<<c;//将字符输入f2中
}

==必须掌握==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*写文件*/
ofstream outf("score.txt", ios::out);
if (!outf) {
cout << "打开文件失败";
return 0;
}
while(cin >> number >> name >> score)
outf << number << " " << name << " " << score<<endl;
outf.close();

/*读文件*/
ifstream f("score.txt",ios::in);
char a[20], b[20], c[20],d[20];
for (int i = 0; i < 2; ++i) {
f >> a >> b >> c>>d; // 使用 >> 操作符读取字符
std::cout << a << " " << b << " " << c <<" "<<d<< std::endl;
}

f.close(); // 正确的关闭文件流的方法

完整用例

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
int main() {
fstream file;
file.open("1.txt", ios::out); // 以只写模式打开文件

// 写入字符串 "hello world!" 到文件中,不包括结束符 '\0'
file.write("hello world!\n", 13);
file.write("hello world!2", 13);

// 为了安全起见,检查 write 是否成功
if (!file) {
cerr << "Error writing to file." << endl;
return 1;
}

// 关闭文件,准备重新以读模式打开
file.close();

// 以读模式重新打开文件
file.open("1.txt", ios::in);
if (!file) {
cerr << "Error opening file for reading." << endl;
return 1;
}

// 使用 getline 读取一行,最多19个字符,留一个位置给结束符 '\0'
char str[20];
file.getline(str, sizeof(str) - 1);

// 输出读取的字符串
cout << str << endl;
file.getline(str, sizeof(str) - 1);

// 关闭文件
file.close();

return 0;
}

十一、模板

11.1函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

template<typename T, typename U>
void show(T a, U b) {
cout << "T: " << a << " " << "U: " << b << endl;
}

int main() {
show(1, 1);
show(0.1, 0.1);
return 0;
}

11.2类模板

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
#include <iostream>
#include <vector>

#define MAX_SIZE 1024

using namespace std;

template <typename T>
class Array {
private:
int len;
T data[MAX_SIZE];
public:
Array() {
len = 0;
memset(data, 0, sizeof(data));
}

void add(T element) {
data[len] = element;
len++;
}

void show() {
for (int i = 0; i < len; i++) {
cout << data[i] << " ";
}
cout << endl;
}
};

int main() {
Array<int> array_int;//此处记得要给出参数列表

array_int.add(1);
array_int.add(2);
array_int.add(3);
array_int.show(); // 1 2 3

Array<string> array_str;
array_str.add("a");
array_str.add("b");
array_str.add("c");
array_str.show(); // a b c

return 0;
}

十二、异常处理

12.1 throw-try-catch

throw 语句用于抛出一个异常。这个异常可以是任何类型的值或对象。

1
throw expression;

try 块包含可能抛出异常的代码。catch 块用于捕获并处理异常。

1
2
3
4
5
6
7
try {
// 代码块,可能抛出异常
} catch (type1 arg1) {
// 处理 type1 类型的异常
} catch (type2 arg2) {
// 处理 type2 类型的异常
} // 可以有多个 catch 块
  • throw 语句会立即终止当前的代码块,并跳转到与之最近的 catch 块。

  • 如果 throw 语句在一个 try 块内部,那么它会跳转到这个 try 块后面的 catch 块。如果 throw 语句不在任何 try 块内部,或者没有找到匹配的 catch 块,那么程序会调用 std::terminate 函数,通常会导致程序终止。

  • catch 块的参数类型必须与 throw 语句抛出的异常类型匹配。如果有多个 catch 块,那么会选择第一个匹配的 catch 块。

  • catch 块的参数是一个新的局部变量,它的值是 throw 语句抛出的异常值的副本。如果异常是一个对象,那么这个对象会被复制。为了避免复制的开销,通常会抛出和捕获异常对象的引用。

  • 如果异常是一个类类型,那么可以使用基类来捕获所有的派生类异常。例如,catch (const std::exception& e) 可以捕获所有派生自 std::exception 的异常。

  • 可以使用 catch (...) 来捕获所有类型的异常,但是这样做无法获取到异常的具体信息。通常,catch (...) 会在最后一个 catch 块,用于捕获其他 catch 块没有捕获到的异常。

  • catch 块中,应该避免再次抛出异常,除非你打算在更高的层次处理这个异常。如果在 catch 块中抛出了新的异常,那么原来的异常就会丢失。

  • 异常应该用于处理真正的异常情况,而不是用于控制正常的程序流程。过度使用异常会使代码难以理解和维护,也会影响程序的性能。


十三、理论性表述梳理

  • 面向对象程序设计特点包括封装性,继承性,多态性。

  • 构造函数和析构函数都不可以继承。final说明的类不能被继承。友元函数不会被继承。

  • 虚函数

    • 只能是成员函数。
    • 基类和派生类函数名和参数列表必须相同。
    • 抽象类指的是包含纯虚函数的类,抽象类无法用于实例化对象。
    • 为了实现运行时多态性,派生类应该使用公有继承
  • 构造析构顺序

    • 当创建一个派生类的对象时,首先会调用基类的构造函数,然后是派生类的构造函数。
    • 当销毁一个派生类的对象时,首先会调用派生类的析构函数,然后是基类的析构函数。
  • 友元函数:

    • 如果函数 fun()被说明为类 A 的友元,那么在 fun()中可以访问类 A 的私有成员
    • 友元关系不能被继承
    • 友元破坏了类的封装性
    • 尽管友元函数可以访问类的私有和保护成员,但它仍然不是类的成员函数,因此它不能使用类的this指针
  • 函数重载

    • 函数重载是根据函数的参数列表来确定的
    • 参数列表中参数的数量,类型和顺序
    • 属性(Attribute):用于描述对象的静态特征。
    • 方法(Method):用于描述对象的动态特征。
  • 派生类

    • 派生类不能继承基类的任何构造函数。派生类必须定义自己的构造函数,并在需要时显式调用基类的构造函数。
  • 只能作为类的成员函数重载的运算符包括:

    • 赋值运算符 (=)
    • 函数调用运算符 (())
    • 下标运算符 ([])
    • 成员访问运算符 (->)

    这些运算符在重载时必须定义为类的成员函数,因为它们需要直接访问类的内部结构或成员。

  • 局部变量位于栈,全局变量和静态变量位于数据段(静态区)。mallocnew申请的空间为堆区。

  • 模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换。

  • 类模板中的成员函数全是模板函数。

  • const int b; float* &c;只能通过参数列表初始化。