C++笔记 面向对象 初学

C的过渡

引用类型

int c, d;
int &a = c;
const int &b = c;
b = d;

常量引用类型不是该变量的值不能改变,而是它无法改变指向的变量的值(同样的,常量指针类型变量也是如此)。

int a, b;
const int *p;
p = &a;
p = &b;//合法
*p = 666;//不合法

特别注意,引用类型声明的时候必须初始化,单纯int &a;是非法的。

还需记忆一下类型转换:

  • T可以隐性转化为const T
  • const T不可以给T类型赋值,但是可以通过(T)强制类型转换来实现
const int a = 114;
int &b = (int&)a;
cout << b;

const int a = 514;
int *b  = (int*) &a;
cout << *b;
//若去掉强制类型转换则无法通过编译

此外还需特别提及一下指针的引用

int *a, *b, *c;//虽然我们知道此指针的类型是int *,但是为什么SB的c还得每个变量前都加*呢?
void function(int* &a, int* &b){}//出现了一个抽象的顺序,引用指针竟然不是&*反而是*&,这也恰恰证明int *确确实实是一个type,所以指针的引用是这么写滴。

顺便搬运一下几种指针看法:

“ * ”的优先级低于“ ( )”的优先级

int *a;
你只需要看右边,*a 一定是一个 int 类型。
int (*a)(int);
你把 (*a) 看做一个整体,就知道,(*a) 一定是一个返回 int,接受一个 int 参数的函数,所以 a 是函数指针。
int *a[5];
*a[i] 是 int,所以 a[i] 一定是 int*,所以 a 是一个数组,数组里的每一个元素都是 int*。
int (*a)[5];
(*a)[i] 是 int,所以 (*a) 是一个 int 数组,a 是指向数组的指针。
int (*a[5])(int);
(*a[i]) 是一个返回 int,接受一个 int 参数的函数,所以 a[i] 是一个函数指针,所以 a 是函数指针的数组。

注:使用当函数指针传参的时候需要函数名和参数表一起传入。
如:

ostream& operator<<(ostream& (*func)(ostream&)) {
    return func(*this);
}

动态内存分配

T *p;
//分配变量
p = new T;
delete P;
//分配数组
p = new T[N];
delete [] p;

我们通过二维动态数组的分配来加深理解:

对于指针pp[i]等价于*(p + i)

int r, c;
int **p = new int[r];
for(int i = 0; i < r; i++) {
    p[i] = new int[c];
}
//do something
for(int i = 0; i < r; i++) {
    delete [] p[i];
}
delete [] p;

函数

内联函数

在函数前加上inline,当频繁调用一个代码量较少的函数时,可以通过替换代码,减少函数栈的使用,加快速度。

注意:

  1. 关键字inline 必须与函数定义体放在一起才能使函数成为内联,声明时的inline不起作用。
  2. 内联函数本身不能是直接递归函数。

重载函数

当函数名称相同时,参数表不同时,编译器可以自动判断参数类型来实现调用特定函数。

注意:名称和参数表相同,但返回值类型不同的函数是重复函数,而非重载函数,是错误的。

缺省参数

英文很好理解就是default

当传参小于需要的参数个数时,函数的默认参数

void Example(int a, int b = 5, int c = 6){}
Example(1, 2, 3);
Example(1, 2);
Example(1);
//注意只能最右面的连续若干个参数缺省
Example(1,,3);//这是非法的

函数作为参数

作为后文的补充内容。

我们先讨论以下函数。

return_type function_name (parameters){}

我们初学的时候觉得这样的定义理所当然,但function_name的类型到底是什么呢?它存储的是一个地址跟前面的返回类型没什么关系。所以*function_name才是所谓的函数,平时我们调用的时候也从不写*,因为你不能直接调用一个函数,你需要通过一个指针来调用它。

于是进一步学习函数指针。

type (*f)(parameters);//这是一个常见的声明

想要调用函数,我们需要明确这个函数需要哪些参数,也就是说参数表是函数的一个属性,是它作为辨识的一个特点。所以函数指针的类型是包括参数表的。形如:type* (parameters)。至于为什么是(*f),虽说我们知道指针的类型是type*,但是在(1)括号先于*(2)在 C++ 中,函数的声明和定义的顺序是从右到左的,为了避免函数名和参数表先结合成为返回值为指针的函数,括号是必要的。

有了以上只是作为铺垫,函数作为参数就很容易理解了。将函数指针声明写在函数的参数表里即可。

#include <iostream>
using namespace std;
void Foreach(int* begin, int* end, void (*f)(int)) {
    for (int* p = begin; p != end; ++p) {
        f(*p);
    }
}
//注意:在参数表声明使用auto仅在c++20标准支持,所以void (*f)(int),也可以写作auto f
void Print(int s) {
    cout << s << " ";
}
int a[100];
int main() {
    int n;
    cin  >> n;
    for (int j = 0; j < n; ++j)
        cin >> a[j];
    Foreach(a, a + n, Print);
    cout << endl;
    return 0;
}

但是这样还是太烦琐和局限了,我们可以通过模板来实现传入各种各样的函数指针。

#include <iostream>
using namespace std;
template <typename T>
void Foreach(int* begin, int* end, T f) {
    for (int* p = begin; p != end; ++p) {
        f(*p, *p + 1);
    }
}
void Print2(int s, int b) {
    cout << s << " " << b << " ";
}
int a[100];
int main() {
    int n;
    cin  >> n;
    for (int j = 0; j < n; ++j)
        cin >> a[j];
    Foreach(a, a + n, Print2);
    cout << endl;
    return 0;
}

这样也绝非完美,参数的个数还是收到限制的,即使函数有缺省值,你也必须提供完整参数,否则无法编译通过。想要解决这个问题我们需要使用lamada表达式作为函数参数。

类和对象

基本概念

class 类名 {
    public:
    	公用的成员变量和成员函数
    protected:
    	保护的成员变量和成员函数
    private:
    	私有的成员变量和成员函数
};
//pubic\private\protected出现的次数和先后顺序没有限制
//若没有上述关键词,则被缺省地认为是private
  • public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
  • protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
  • private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。

类的成员:变量,函数,对象

类是当结构化程序实现功能时过于繁琐时的替代产物,相当于把一类事物抽♂象出来,每一类事物一定有它的属性,和它的各种行为,我们将其用成员变量和成员函数来表达这一类事物。

对象是根据类来定义的,通过类名 对象名;来声明。

类的函数和之前提及的函数一样,也可以实现重载,缺省,它的声明在类里,但是它的定义可以在类外实现,如下:

using namespace std;
class Ex {
    public:
        double a,b,c;
        void output();
    private:
    	int a_function();
};
int Ex::a_function(int x, int y){do sth};
void Ex::output() {
    cout << a << " " << b << " " << c << endl;
    //和在类里面一样访问成员变量
}

此外类类似于struct也可以通过X.x和指针p -> x进行访问。

关于初始化:

在 C++ 中,你可以在类的定义中为非静态成员变量提供一个默认的初始化器。这是一个例子:

class MyClass {
public:
    int flag = 1;
};

在这个例子中,每当创建一个 MyClass 的实例时,成员变量 flag 都会被初始化为 1

请注意,这个特性在 C++11 及以后的版本中可用。如果你使用的是 C++11 之前的版本,你需要在类的所有构造函数中初始化 flag。例如:

class MyClass {
public:
    int flag;

    MyClass() : flag(1) {}
};

在这个例子中,flagMyClass 的构造函数的初始化列表中被初始化为 1

构造函数

你可知道构造函数的N种类型!(移动构造,委托构造std=c++11 to be continued)

和类名相同并且无任何返回的函数。(无任何返回不是返回void)

默认生成无参数的(复制)构造函数。

构造函数:类名 (参数表)

复制构造函数:类名 (类名(常)引用类型)

(显式)类型转换构造函数:(explicit) 类名 (单个参数)。注:尽量使用显式,防止意外之喜。此外显式和隐式类似于原生的类型转换。

例:

using namespace std;

class Ex {  
    public:
    	int a, b, c;
        Ex(int _a, int _b, int _c);
        void output();
    	Ex(const Ex &k) {
            a = k.a;
            b = k.b;
            c = k.c;
            cout << "copy" << endl;
        }
    	Ex(int k) {
            a = k;
            b = k + 1;
            c = k + 2;
            cout << "transform" << endl;
        }
};

Ex::Ex(int _a, int _b, int _c = 6) {
    a = _a;
    b = _b;
    c = _c;
}
void Ex::output() {
    cout << a << " " << b << " " << c << endl;
}

int main(){
	Ex a(1, 2, 3);
	a.output();
	Ex b(5,6);//缺省构造
	b.output();
    
    Ex c = 6;
    Ex d(7);//类型转换构造
}

调用复制构造函数的三种情况

  1. 初始化时,以下两种方式等价Ex a;Ex b(a); Ex a; Ex b = a;.
  2. 传形参,传入函数的参数为类,且不是引用类型时
  3. 函数返回类型为类

注意,对象之间赋值不调用构造函数,只有初始化的时候才调用。

Ex a, b; b = a该条语句就不会调用。

最后,有以上第2条可知,传参的时候存在开销,所以可以采用引用类型进而不调用复制构造函数,若确保原变量不被改变,则传入参数为常引用类型。

此处应有一道例题:

#include <iostream>
using namespace std;
class Sample {
public:
	int v;
	Sample(int n) { v = n; }
	Sample() { v = 0; }
	Sample(Sample& b) { v = 2 + b.v; }
	void PrintAndDouble();

};
void PrintAndDouble(Sample o)
{
	cout << o.v;
	cout << endl;
}
int main()
{
	Sample a(5);
	Sample b = a;
	cout << b.v << endl;  
	PrintAndDouble(b);
	Sample c = 20;
	PrintAndDouble(c);
	Sample d;
	d = a;
	cout << d.v;
	return 0;
}
/**
output:
7
9
22
5 
**/

关于复制构造函数在不同编译器上的情况:

代码如下:

#include <iostream>

using namespace std;

class A {
	public:
		int x;
		A(int x_) : x(x_) {
			cout << x << " constructor called" << endl;
		}
		A(const A &a) {
			x = 2 + a.x;
			cout << "copy called" << endl;
		}
		~A() { cout << x << " destructor called" << endl; }
};
A f() {
	A b(10);
	return b;
}
int main() {
	A a(1);
	a = f();
	return 0;
}

在g++上(进行了优化):

1 constructor called
10 constructor called
10 destructor called
10 destructor called

在vs的cl上:

1 constructor called
10 constructor called
copy called
10 destructor called
12 destructor called
12 destructor called

析构函数

写法:~ 类名(){}

对象消亡时调用。

注意:new出来的对象不delete就不会消亡

this指针

this指针的存在我们可以理解为,早期编译过程C++ – > C -->机器码,而class的存在无疑和c中的struct的相似,而c的struct没有实现类似成员函数的功能,所以函数只能写在struct外,进而为了知道我们修改的是哪一个成员,我们便需要一个this指针:其作用就是指向成员函数所作用的对象 。

以下是一份C++代码:

#include <iostream>
using namespace std;

class Car {
public:
    int price;
    void SetPrice(int p);
};
void Car::SetPrice(int p) {
    price = p;
}
int main() {
    Car car;
    car.SetPrice(20000);
    return 0;
}

翻译成C语言则如下:

#include <stdio.h>

typedef struct {
    int price;
} Car;

void SetPrice(Car* this, int p) {
    this -> price = p;
}

int main() {
    Car car;
    SetPrice(&car, 20000);
    return 0;
}

非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。而静态成员函数没有对应的对象,所以无法用this调用。但是调用的函数一定不能访问成员才有的值,否则会报错。

进而我们可知,非静态成员函数的参数比已知的隐性多一个,而静态成员函数的参数就是你写上去的那些。

例:

#include <iostream>
using namespace std;

class A { 
public:
    int i;
    void Hello() { cout << "hello" << endl; }
    void crash() { cout << i << endl; }
};
int main() {
    A* p = NULL;
    p->Hello();
    p->crash();//crash
}

静态成员

类的静态变量类似于全局变量,namespace为类。又因为它时共用的所以不占用单个对象的内存,sizeof(类)不包括其大小

类的静态成员函数没有this指针,只能访问静态成员。

静态成员,所有对象都可以对其进行访问,静态成员只要使用类名加范围解析运算符 :: 就可以访问。

#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;

public:
    static int personCount;
    static void displayCount() {
        std::cout << "Person count: " << personCount << std::endl;
    }
    Person(std::string name, int age) : name(name), age(age) {
        personCount++;
    }
    Person(const Person& person) {
        name = person.name;
        age = person.age;
        personCount++;
    }//对复制进行重载,不遗漏情况
    ~Person() {
        personCount--;
    }
    void displayInfo() {
        std::cout << "Name: " << name << std::endl;
        std::cout << "Age: " << age << std::endl;
    }


};

int Person::personCount = 0;
int main() {
    Person person1("John Doe", 25);
    Person person2("CBK", 24);
    std::cout << Person::personCount << std::endl;
    Person::displayCount();
    return 0;
}

注意:静态成员变量一定要进行初始化,而ISO C++禁止在类的内部初始化非常量,所以不要忘记在类的外部初始化。

对静态成员变量时候操作时候,考虑情况要做到不遗漏,考虑构造/析构时的各种情况。

成员对象

成员对象,就是是对象的成员(废话),就是套娃。有成员对象的类就是封闭类。//定义存疑找不到

**任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。 **

与此同时,我们引入C++ 类构造函数的初始化列表。

类名::构造函数名(参数表): 成员变量1(参数表), 成员变量2(参数表), ...{}

初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
1.内置数据类型,复合类型(指针,引用)
在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
2.用户定义类型(类类型)
结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)

有的时候必须用带有初始化列表的构造函数:

  • 1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
  • 2.const 成员引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。

引用

封闭类构造函数和析构函数的执行顺序 :

  • 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。
  • 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关。
  • 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。

例:

#include <iostream>
using namespace std;
class Tyre {  //轮胎类
private:
    int radius; //半径
    int width; //宽度
public:
    Tyre(int r, int w) :radius(r), width(w) {}
};
class Engine {};//发动机类
class Car { //汽车类
private:
    int price; //价格
    Tyre tyre;
    Engine engine;
public:
    Car(int p, int tr, int tw):price(p), tyre(tr, tw){};
};
int main() {
    Car car(20000, 17, 225);
    return 0;
}

上例中,如果 Car类不定义构造函数, 则Car car;会编译出错:
因为编译器不明白 car.tyre该如何初始化。 car.engine 的初始化没问题,用默认构造函数即可 。

友元

友元分为友元函数和友元类两种

友元函数: 一个类的友元函数可以访问该类的私有成员 ,可以将一个类的成员函数(包括构造、析构函数)说明为另一个类的友元。(该函数也可以不是任何类的成员函数)(友元函数无this指针)(友元函数即使是全局函数也可以写在类里,且参数个数正常)

友元类: 如果A是B的友元类,那么A的成员函数可以访问B的私有成员。

友元类之间的关系不能传递,不能继承。

友元函数

例:

#include <iostream>
using namespace std;
class CCar;
class CDriver {
public:
    void ModifyCar(CCar* pCar); //改装汽车
};
class CCar {
private:
    int price;
    friend int MostExpensiveCar(CCar cars[], int total); //声明友元
    friend void CDriver::ModifyCar(CCar* pCar); //声明友元
};
void CDriver::ModifyCar(CCar* pCar) {
    pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total) {
    int tmpMax = -1;
    for (int i = 0;i < total; ++i)
        if (cars[i].price > tmpMax)
            tmpMax = cars[i].price;
    return tmpMax;
}


class B {
public:
    void function();
};
class A {
    friend void B::function();
};

int main() {
    return 0;
}

友元类

class CCar {
    private:
    int price;
    friend class CDriver; //声明CDriver为友元类
};
class CDriver {
    public:
    CCar myCar;
    void ModifyCar() {//改装汽车
        myCar.price += 1000;//因CDriver是CCar的友元类,
        //故此处可以访问其私有成员
    }
};
int main(){ return 0; }

常量成员函数

对于常量对象只能使用构造函数、析构函数和 有const 说明的函数(常量方法)

在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数。 常量成员函数内部不能改变属性的值,也不能调用非常量成员函数。即该函数对于成员变量,只可读不可写。

注意:

  1. 定义常量成员函数声明常量成员函数时都应该使用const 关键字。
  2. 成员变量前有 mutable,则在常量成员函数中,也可修改成员变量
class Sample {
private :
    int value;
    mutable int value2;
public:
    void PrintValue() const;
};
void Sample::PrintValue() const { //此处不使用const会导致编译出错
    value2 ++;//合法
    cout << value;
}
void Print(const Sample & o) {
    o.PrintValue(); //若 PrintValue非const则编译错
}

常量成员函数的重载 😗*两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。 **

#include <iostream>
using namespace std;

class Sample {
private :
    int value;
public:
    void PrintValue() const;
    void PrintValue();
    Sample(int v) : value(v) {}
};
void Sample::PrintValue() const {
    cout << value << endl;
}
void Sample::PrintValue() {
    cout << value * 2 << endl;
}
int main() {
    Sample o(114);
    const Sample co(514);
    o.PrintValue();
    co.PrintValue();
    return 0;
}

运算符重载

形式为:类型 operator 运算符 (参数表) (const可选){}

进行重载的时候要考虑好返回类型和基本逻辑,如是否满足交换,连续运算,链式赋值等情况。

运算符重载为成员函数时,隐含this指针。

例:

//假设有Complex类,包含 real imag
Complex operator + ( const Complex & a, const Complex & b) {
	return Complex( a.real+b.real,a.imag+b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c) {
	return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}//Complex a, b, c;
//c = a + b; 等价于c = operator + (a, b);
//a - b 等价于a.operator - (b)

=只能重载为成员函数

下面是不可重载的运算符:

  • .:成员访问运算符
  • .*, ->*:成员指针访问运算符
  • :::域运算符
  • sizeof
  • ?::条件运算符
  • #: 预处理符号

我们通过实现简易string类和complex类来对其加深理解。

#include <iostream>
#include <cstring>

using namespace std;

class String {
    char* pstr;
public:
    friend ostream& operator<<(ostream& os, const String& s);
    String() : pstr(new char[1]) { pstr[0] = 0; }
    String(const char* h) {
        int len = strlen(h);
        pstr = new char[len + 1];
        strcpy(pstr, h);
    }
    String(const String& s) {
        int len = strlen(s.pstr);
        pstr = new char[len + 1];
        strcpy(pstr, s.pstr);
    }//自定义的赋值构造函数具有其存在必要性,否则当初始化出现=时,是浅复制,可能导致delete两次相同内存。

    String& operator=(const String& other) {
    if (this != &other) {//需防止自己等于自己时产生bug
        delete[] pstr;
        int len = strlen(other.pstr);
        pstr = new char[len + 1];
        strcpy(pstr, other.pstr);
    }
    return *this;
}

    String& operator = (const char* c) {
        if (pstr != c) {
            int len = strlen(c);
            delete[] pstr;
            pstr = new char[len + 1];
            strcpy(pstr, c);
            return *this;
        }
        return *this;
    }
    ~String() {
        delete[] pstr;
    }
};

ostream& operator << (ostream& os, const String& s) {
    os << s.pstr;
    return os;
}

int main() {
    char cbk[] = "abc";
    String a(cbk);
    String b(a);
    String c;
    c = a;//在 C++11 及以后的版本中,如果你定义了一个拷贝构造函数但没有定义一个拷贝赋值运算符,编译器会生成一个警告。
    cout << c << endl;
    //即cout.operator << (c) << endl;
    return 0;
}
#include <iostream>
#include <cstdio>
using namespace std;

class complex {
private:
    double real, imag;
public:
    friend ostream& operator << (ostream& os, const complex& c);
    complex(double r = 0, double i = 0) : real(r), imag(i) {}
    complex operator + (const complex& c2) const {
        return complex(real + c2.real, imag + c2.imag);
    };
    complex operator - (const complex& c2) const {
        return complex(real - c2.real, imag - c2.imag);
    };
    bool operator == (const complex& c2) const {
        return real == c2.real && imag == c2.imag;
    };
    bool operator > (const complex& c2) const {//复数无法比较大小,只是比较模长
        return real * real + imag * imag > c2.real * c2.real + c2.imag * c2.imag;
    }; 
};
ostream& operator << (ostream& os, const complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;
}
int main() {
    complex c1(1, 2), c2(3, 4);
    cout << c1 << c2;
    return 0;
}

注:

  1. 类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型
  2. 全局函数有时需要友元来访问对象私有属性,虽然很破坏封装性
  3. 注意自增自减运算符的前置后置,如在成员函数内,前置T & operator++(); ,后置T operator++(int);
    此处的int不代表类型,只是为了向编译器说明这是一个后缀形式,而不是表示整数。

对于可变长度的数组可以利用到[]的重载,核心在于int&类型的返回是的a[i] = kk = a[i]均能成立。

class IntArray {
private:
    int* data;  // 指向动态数组的指针
    int size;   // 数组的大小

public:
    // 构造函数
    IntArray(int size) : size(size) {
        data = new int[size];
    }

    // 拷贝构造函数
    IntArray(const IntArray& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
    }

    // 拷贝赋值运算符
    IntArray& operator=(const IntArray& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            for (int i = 0; i < size; i++) {
                data[i] = other.data[i];
            }
        }
        return *this;
    }

    // 析构函数
    ~IntArray() {
        delete[] data;
    }

    // 获取数组元素
    int& operator[](int index) {
        return data[index];
    }

    // 获取数组大小
    int getSize() const {
        return size;
    }
};//本代码由ai生成,供参考

特别神奇的备注:对于二维动态数组数组,[]的返回类型是int*

int *operator[](int i) { return p[i]; },这是由于[]的顺序是从左到右的,然后第二次进行运算时不进行重构!

继承和派生

C++类之间存在四种关系(old school):

  1. 没有关系(乐
  2. 继承关系:A,B两个类,B 是 A,B就可以通过继承A,成为A的派生类,减少代码重复。如男人和人之间的关系。
  3. 复合关系:前文的封闭类一样,类中有类成员
  4. 委托关系:类中包含其他类的指针。两个类可以通过互相委托,但是无法互相复合。

对于继承的设计要符合逻辑,判断B到底是不是A。只是包含的关系应采用复合关系。

继承

基本写法:class derived-class: access-specifier base-class

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono

这里我们可以知道**protected的范围比private**大一点点。

  • protected继承时,基类的public成员和protected成员成为派生类的protected成员。
  • private继承时,基类的public成员成为派生类的private成员,基类的protected成员成
    为派生类的不可访问成员。

此外派生类的成员函数中可以访问非this对象的protected成员。但是注意无法修改基类的protected成员,只能修改派生类从基类继承过来的成员,如下面例子中的参数只能是B b而不能是是A a

#include <iostream>
using namespace std;

class A {
protected:
    int x;
};

class B: public A {
public:
    void set_print(B b, int i) {
        b.x = i;
        cout << b.x << endl;
    }
};

int main() {
    B b;
    b.set_print(b, 10);
    return 0;
}

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承可以继承很多次,派生类具有直接基类和间接基类,我们只需要列出它的直接基类就会自动向上继承它的间接基类

派生类的成员包括自己,直接和间接基类全部成员。

例:

#include <iostream>
#include <string>
using namespace std;

class Student {
protected:
    string name;
    int age;
    int gender;
public:
    void show_info() {
        cout << "name: " << name << "age:" << age << endl;
    }
};

class Undergraduate : public Student {
    int score;
public:
    void show_info() {
        Student::show_info();//调用父类的show_info,已经实现一部分功能
        cout << "score: " << score << endl;
    }
};

构造和析构

派生类的内存大小 = (基类 + 自己)的内存大小

构造和析构的顺序类似于封闭类,在创建派生类的对象时:

  1. 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
  2. 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
  3. 最后执行派生类自己的构造函数

消亡时:

  1. 先执行派生类自己的析构函数
  2. 再依次执行各成员对象类的析构函数
  3. 最后执行基类的析构函数

如何正确地构造派生类呢?

就以CT(counter terrorist)为例子,通过列表初始化调用基类地构造函数,这样就可以避免私有成员地无法访问。

class Person {
    int health, money;
public:
    Person(int h, int m) : health(h), money(m) {}
};

class equipment {
    int weapon, item;
public:
    equipment(int w, int i) : weapon(w), item(i) {}
};

class CT : public Person {
    equipment eq;
public:
    CT(int h, int m, int w, int i) : Person(h, m), eq(w, i) {}
};

覆盖

派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::

public赋值情况

class base { };
class derived : public base { };
base b;
derived d;
  1. 派生类的对象可以赋值给基类对象
    b = d;
  2. 派生类对象可以初始化基类引用
    base & br = d;
  3. 派生类对象的地址可以赋值给基类指针
    base * pb = & d;

注意:如果派生类型是privateprotected则不可以。

多继承

已经属于抽象的了,但对于一个对象B确实可以是A也可以是C,正如在学校是学生,在家时孩子一样。

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,{
<派生类类体>
};

复合和委托

经典案例:

class CPoint {
    double x, y;  //点的坐标
};
class CCircle: public CPoint{
    double radius;  //半径
};//#1
class CCircle {
    CPoint center;  //圆心
    double radius;  //半径
};//#2

对于#1的继承,看似省事,但是是不符合逻辑的,只是功能上的实现,#2才是合理的写法。

然后就是提及到的小区人狗管理系统。我们既需要知道主人有哪几条狗,也需要知道狗的主人是谁。
但是如果我们尝试用复合关系去写呢?我们就会发现出现循环定义的问题。

class CDog;
class CMaster {
    CDog dogs[10];
};
class CDog {
    CMaster m;
};

当我们尝试计算sizeof(CDog)就可以显然发现这个错误。然后我们进行第一次修改。

class CDog;
class CMaster {
    CDog* dogs[10];
};
class CDog {
    CMaster m;
};

这此无疑可以通过编译,但是对于好几条同一个主人的狗,他们的m意义是相同的,但是内存是不同的,当我们对主人进行操作时,状态无法对这么多狗轻松进行同步,所以还是不合理的。我们进行第二次修改。

class CMaster;
classCDog {
    CMaster* m;
};
class CMaster {
    CDog dogs[10];
};

这种就是人中有狗的复合关系,但是对于复合关系的定义:B是A的固有属性或组成部分是违和的,狗当人不是人的一部分。此外这种写法的缺陷在于,所有的狗都是在CMaster里面定义的,对狗的一切操作都得在CMaster里面进行,Dogs' lives matter!,这种写法无疑是不合理的。

正确的写法是两者互相委托。对狗的操作直接在外部就可以实现。

class CMaster;
classCDog {
    CMaster* pm;
};
class CMaster {
    CDog *dogs[10];
};

多态

通过虚函数,我们可以实现在派生类和基类中调用同名函数(同参数表)动态地根据对象类型,调用该对象对应地参数。

虚函数

前面有virtual关键词声明的类的成员函数就是虚函数,该关键词只需在声明的时候使用,定义的时候不需要。

对于派生类中同名同参函数不必须使用vitrual进行声明,它也会是虚函数。

虚函数的调用:

  1. 通过基类指针调用派生类的虚函数

    class CBase {
        public:
        virtual void SomeVirtualFunction() { }
    };
    class CDerived:public CBase {
        public :
        virtual void SomeVirtualFunction() { }
    };
    int main() {
        CDerived ODerived;
        CBase * p = & ODerived;
        p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
        return 0;
    }
    
  2. 通过基类引用调用派生类的虚函数

    //类定义同上
    int main() {
        CDerived ODerived;
        CBase& r =  ODerived;
        r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
        return 0;
    }
    

纯虚函数和抽象类

纯虚函数样例:virtual T function() = 0;

包含纯虚函数的类叫抽象类:

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
  • 抽象类的指针和引用可以指向由抽象类派生出来的类的对象

抽象类的成员函数为什么可以调用纯虚函数呢?因为抽象类一定不能生成它的对象,即使在成员函数中调用该虚函数也一定是在派生类中实现的情况。

此外虚函数必须实现,如果不实现,编译器将报错。

一个派生类想要不是抽象类,则必须实现基类的所有虚函数

意义与开销

多态是通过时间和空间成本来减少开发人员繁琐程度的。以下内容仅为胡扯。

在空间上:如果我们使用sizeof()我们就会发现抽象类比正常类多8个字节,需要存储虚函数表。

在时间上:每一次调用都需要判断,显而易见()

但是当我们想要对程序进行更新时,比如存在对派生类之间进行交互的函数,如果不采用多态的写法,我们就需要穷举每两个类之间的所有行为,无疑是冗杂又麻烦的,如果采用多态,只需要用基类作为参数,然后在不同派生类中实现相似的虚函数就可以。

析构函数

虚函数只存在虚析构函数,不存在虚构造函数。

其意义在于:通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

解决办法:把基类的析构函数声明为virtual

  • 派生类的析构函数可以virtual不进行声明
  • 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

最后附上一个例子:

#include <iostream>
using namespace std;
class A {
    private:
    int nVal;
    public:
    void Fun()
    { cout << "A::Fun" << endl; };
    virtual void Do()
    { cout << "A::Do" << endl; }
};
class B:public A {
    public:
    virtual void Do()
    { cout << "B::Do" << endl;}
};
class C:public B {
    public:
    void Do( )
    { cout <<"C::Do"<<endl; }
    void Fun()
    { cout << "C::Fun" << endl; }
};
void Call(A* p) {
    p->Fun(); p->Do();
}
int main() {
    Call( new A());
    Call( new C());
    return 0;
}

output:

A::Fun
A::Do
A::Fun
C::Do

输入输出暨文件处理

不得不说cin cout性能低下()

我们熟悉的iostream是由istreamosteam这两个类派生而来。

istream是用于输入的流类, cin是该类的对象。
ostream是用于输出的流类, cout是该类的对象。
iostream是既能用于输入,又能用于输出的类。
ifstream是用于从文件读取数据的类。
ofstream是用于向文件写入数据的类。
fstream 是既能从文件读取数据,又能向文件写入数据的类。

标准流对象

1 .cin,表示标准输入的istream类对象。cin使我们可以从设备读如数据。
2. cout,表示标准输出的ostream类对象。cout使我们可以向设备输出或者写数据。
3 .cerr,表示标准错误的ostream类对象。cerr是导出程序错误消息的地方,它只能允许向屏幕设备写数据。

  1. clog,也是表示标准错误的ostream类对象。clog是标准日志流,也是只能向屏幕输出。
    对于cerr和clog两者,cerr不使用缓存区,clog使用缓冲区。

在进行文件输出重定向后,进行调试或抛出异常的时候便应该使用cerr或clog,这样信息便不会写入文件中。

istream类的成员函数

  1. istream & getline(char * buf, int bufSize);
    istream & getline(char * buf, int bufSize,char delim);

    delim表示分隔符,getline只会读入bufSize - 1个字符,或者碰到delim\n(有一个到的就截止),并在结尾自动添加'\0'\ndelim不会被读入。

    getlinecin也可以通过while(cin >> x)判断是否读入,具体原理下文系嗦。

  2. bool eof(); 判断是否结束

  3. int peek();返回下一个字符,但不从输入流中去掉

  4. istream & putback(char c);将字符c放入流

  5. istream & ignore(int n = 1, int delim = EOF);从流中最多删除n个字符,遇到delim时结束。

重定向

事实上这是c的库。

FILE *freopen(const char *filename, const char *mode, FILE *stream)

模式描述
“r”打开一个用于读取的文件。该文件必须存在。
“w”创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。
“a”追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
“r+”打开一个用于更新的文件,可读取也可写入。该文件必须存在。
“w+”创建一个用于读写的空文件。
“a+”打开一个用于读取和追加的文件。

FILE *stream可以为stdinstdout

流操纵算子

使用流操纵算子需要#include <iomanip>

  • 整数流的基数:流操纵算子dec,oct,hex, setbase
int n = 10;
cout << n << endl;
cout << hex << n << '\n' << dec << n << '\n' << oct << n << endl;  
/*
10
a
10
12
*/
  • 浮点数的精度(precision, setprecision)

precision是成员函数,其调用方式为:
cout.precision(5);
setprecision 是流操作算子,其调用方式为:
cout << setprecision(5); // 可以连续输出

保留位数和我们常见的一样分为有效位数和小数点后几位,如果想要用小数点后几位输出就需要setiosflags(ios::fixed) ,若取消则resetiosflags(ios::fixed)

如:cout << setiosflags(ios::fixed) << setprecision(6) << x << endl << resetiosflags(ios::fixed) << x ;

fixed可以起到一个效果。

int main() {
    double a = 114.12345678;
    double b = 114514.191981;
    cout << fixed << setprecision(6) << a << endl;
    cout << b;
}
/*
114.123457
114514.191981
*/

可见保留位数也作用于下方输入。

  • 设置域宽(setw, width)

setw(int n)是流操作算子,width(int n)是成员函数。

**宽度设置有效性是一次性的,在每次读入和输出之前都要设置宽度。 **

  • 用户自定义的流操纵算子
ostream &tab(ostream &output){
    return output << '\t';
}
//调用:
cout << "aa" << tab << "bb";
//输出:
//aa	bb

能这样自定义在于iostream类对<<进行了重载。

ostream & operator <<( ostream & ( * p ) ( ostream & ) ) ;
该函数内部会调用p所指向的函数,且以 *this 作为参数 。(此处若不太理解,请看函数指针传参进行复习)

最后我们讨论以下为什么while(cin >> x)是可以判断是否读入完毕的,while里面是布尔表达式,但是cin无疑是一个istream类型,(插入一句C89的布尔表达式类型竟然是int),所以istream类里面存在着隐式类型转换。此外在C++98和C++11上还存在不同。详细请看

文件

  1. 首先我们需要创建一个文件对象,类为ifstream/ofstream/fstream
  2. 我们既可以采用初始化构造函数,也可以使用open成员函数,初始化对象。

fstream: 读/写模式打开文件, 如果文件不存在,以只读模式打开可以创建新文件,以读/写或写模式不能创建空文件

//读.1
ifstream file;
file.open("文件名", ios::openmode);
//读.2
ifstream file("文件名", ios::openmode);
//读写
fstream file("file.dat");
//ios::openmode即可以缺省,也可以多个连用。
ofstream file("f.dat", ios::out|ios::binary);
模式标志描述
ios::app追加模式。所有写入都追加到文件末尾。
ios::ate文件打开后定位到文件末尾。
ios::in打开文件用于读取。
ios::out打开文件用于写入。
ios::trunc如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。
ios::binary以二进制方式打开。

ios::binary方式打开,换行符被解释成\r\n,反之,换行符被解释成\n

  1. 输入输出

我们同样采用<<>>进行操作,但是作用的对象不再是cincout,而是我们上文中声明的对象。但是<<只能被ofstreamfstream类调用,同样的>>ifstreamfstream类调用。

  1. 文件位置指针

分为读指针和写指针。(特意声明下面函数参数通常是一个长整,在windows上long和int的大小是相同的,但却是是两种类型,11L是两种类型,但我相信它会有隐性类型转换的doge)

对于读指针:

ifstream fin("ex.dat");
long location = fin.tellg();//获得位置
fin.seekg(10L);//移动到第10个字节处,缺省的ios::beg
fin.seekg(location,ios::beg); //从头数location
fin.seekg(location,ios::cur); //从当前位置数location
fin.seekg(location,ios::end); //从尾部数location

对于写指针:

ofstream fout("a1.out");
long location = fout.tellp(); //取得写指针的位置
location = 10;
fout.seekp(location); // 将写指针移动到第10个字节处
fout.seekp(location,ios::beg); //从头数location
fout.seekp(location,ios::cur); //从当前位置数location
fout.seekp(location,ios::end); //从尾部数location

location可以为负数。我们可以注意到对于读和写只有g(get)和p(put)的区别。

  1. 显式关闭文件

对于从c过渡到c++的人来说,等作用于fclose(*fp)close()不再是必须的,但是我们仍可以调用:

fstream f("ex.dat");
f.close();
  1. 二进制文件读写

ifstream 和 fstream的成员函数:
istream& read (char* s, long n);
将文件读指针指向的地方的n个字节内容,读入到内存地址s,然后将文件读指针向后移动n字节。

ofstream 和 fstream的成员函数:
istream& write (const char* s, long n);
将内存地址s处的n个字节内容,写入到文件中写指针指向的位置,然后将文件写指针向后移动n字节。

#include <iostream>
#include <cstring>
#include <fstream>

using namespace std;

struct Student {
    char name[20];
    int score;
};
int main() {
    Student s;
    ofstream OutFile("students.dat",ios::out|ios::binary);
    OutFile.close();

    fstream File("students.dat",ios::in|ios::out|ios::binary);
    while (cin >> s.name >> s.score){
        File.write((char*)&s, sizeof(s));
    }
 
    File.seekp( 2 * sizeof(s),ios::beg); //定位写指针到第三个记录
    char cbk[20] = "cbk";
    File.write(cbk, sizeof(cbk));//写入cbk  20个字节
    int x = 114;
    File.write((char *)(&x), sizeof(int));
    File.close();
    
    File.open("students.dat");
    
    while( File.read( (char* ) & s, sizeof(s) ) ) {
        int readedBytes = File.gcount(); //看刚才读了多少字节
        cout << "readedBytes = " << readedBytes << endl;
        cout << s.name << " " << s.score << endl;
    }
    return 0;
}

出现了莫名其妙的bug,教训我们一定不要瞎寄吧缺省。(ios::binary虽然长还是写上为妙)

模板

对于不同的类型我们做的操作可能是相似的,却又不希望写多个函数进行重载,便可以通过模板来实现。

写在最前面,模板中的classtypename是等价的,class也不是类的意思,只是标识符而已。

模板函数

template<class 参数1, class 参数2, ...>  返回值类型 函数(参数表){}

注意其实template和函数是写在一起的,但是空格对最终表意不影响,所以我们通常将template写在函数的上一行。(这样我们就无需担心作用域的问题)

我们现在就可以实现自己的swap函数了。

template <class T>
void swap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

对于多个参数

template <class T1, class T2>
T2 print(T1 arg1, T2 arg2) {
    cout<< arg1 << " "<< arg2<<endl;
    return arg2;
}

模板类

template <class type> class class-name {}

下面是自己实现的一个pair类。

template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;
    Pair() {}
    Pair(T1 a, T2 b) : first(a), second(b) {}
    bool operator < (const Pair<T1, T2> &p) const;
};

template <typename a, typename b>
bool Pair<a, b>::operator < (const Pair<a, b> &p) const {
    return first < p.first;
}

int main() {
    Pair<int, int> p[2] = {Pair(5, 2), Pair<int, int>(3, 4)};//两种写法都可以,如果编译器无法从上下文中推断出模板参数的类型,你就需要显式地提供模板参数。
    sort(p, p + 2);
    cout << p[0].first << " " << p[0].second << endl;
    return 0;
}

类模板的<类型参数表>中可以出现非类型参数 。

template <class T, int size>
class CArray {
    T array[size];
public:
    void Print() {
        for (int i = 0;i < size; ++i)
            cout << array[i] << endl;
    }
};
CArray<double, 40> a2;
CArray<int, 50> a3;

派生

  1. 类模板从类模板派生
//类模板A
template <class T1,class T2>
class A {
	T1 v1; T2 v2;
};
//由类模板A派生的类模板B
template <class T1,class T2>
class B:public A<T2,T1> {//模板类A派生出模板类B。则,模板类A的参数也由模板类B的参数确定。
	T1 v3; T2 v4;
};
//由类模板B派生的类模板C
template <class T>
class C:public B<T,T>{
	T v5;
};
  1. 类模板从模板类派生
template <class T1,class T2>
class A {
	T1 v1; T2 v2;
};
template <class T>
class B:public A<int,double> {
	T v;
};
int main() {
	B<char> obj1; //自动生成两个模板类: A<int,double> 和 B<char>
	return 0;
}
  1. 模板从普通类派生
//普通类
class A {
	int v1;
};
//由普通类派生的模板诶
template <class T>
class B:public A {//所有从B实例化得到的类, 都以A为基类
	T v;
};

int main() {
	B<char> obj1;
	return 0;
}
  1. 普通类从模板类派生
template <class T>
class A {
	T v1;
	int n;
};

class B:public A<int> {
	double v;
};

int main() {
	B obj1;
	return 0;
}

静态成员

类模板中可以定义静态成员, 那么从该类模板实例化得到的所有类,都包含同样的静态成员 。

#include <iostream>

using namespace std;

#include <iostream>
using namespace std;
template <class T>
class A {
private:
    static T count;
public:
    A() { count++; }
    ~A() { count--; };
    A(A&) { count++; }
    static void PrintCount() { cout << count << endl; }
};

template<> int A<int>::count = 0;
template<> double A<double>::count = 0;
int main() {
    A<int> ia;
    A<double> da;
    ia.PrintCount();
    da.PrintCount();
    return 0;
}

在C++中,模板特化允许你为模板定义一个特殊版本,这个版本只适用于特定的模板参数。模板特化的语法如下:

对于类模板特化:

template <>
class YourTemplateClass<YourSpecializedType> {
    // 特化版本的定义
};

对于函数模板特化:

template <>
ReturnType YourTemplateFunction<YourSpecializedType>(YourSpecializedType arg) {
    // 特化版本的定义
}

在上述代码中:

template<> int A<int>::count = 0;
template<> double A<double>::count = 0;

这是静态成员变量模板特化的例子。A<int>::countA<double>::count 是模板类 A 的特化版本,对于 intdouble 类型的实例,它们的 count 成员变量分别被初始化为 int 类型的 0double 类型的 0

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值