C++课程笔记

翁凯老师C++课程笔记(每一小节的序号略有调整)和 黑马程序员C++课程笔记(泛型编程部分主要记录黑马程序员课程,暂时记录到vector容器~)。

欢迎访问个人网站查看本文,若您发现任何错误,可以通过我的网站中的联系方式与我联系,我将立即修改,谢谢!

1 The First C++ Program

#include <iostream> //文件名不是一定要有后缀的
using namespace std; // Keyword + name

int main(){
    cout << "Hello World! I am" << "18" << "today!" << endl;

    return 0;
}

  输入:cin >>

#include <iostream> 
using namespace std; 

int main(){

    int age;
    cin >> age;
    cout << "You are " << age << ".";

    return 0;
}

擅用refactor: 重构

2 面向对象(Object-oriented)

2.1 什么是面向对象

  Object = Entity

  Object may be visible or invisible

  在程序设计中,对象就是变量。如:int i; i 就是一个对象
(Object is variable in programming languages.)

  C++中,所有的对象都会以变量的形式出现

  对象(Objects)= 属性(Attributes) + 服务(Services)

对象

  数据Data:属性或状态(the properties or status)

  操作Operations:对外提供的服务(the functions)

  对象对外有一个接口

  Mapping(映射):From the problem space to the solution one.

  举例子:上课,定义一个教室,教室里有学生、老师、投影仪、电脑等,它们之间互相有一定的关系,将这个关系描述出来就是面向对象,从存在什么样的东西来分析事情,而不是从发生流程来描述一个事情。

  从某种程度上说,C++中的class就是C中的struct

同一段程序C和C++对比

C Version

typedef struct point3d{
    float x;
    float y;
    float z;
}Point3d;

void Point3d_print(const Point3d* pd);
Point3d a;
a.x=1; a.y=2; a.z=3;
Point3d_print(&a);

C++ Version

class Point3d{
public:
    Point3d(float x, float y, float z);
    print();
private:
    float x;
    float y;
    float z;
};

Point3d a(1, 2, 3); //a是一个对象
a.print(); //operation

  二者不同在于:C++操作在内部,class中有数据有操作。C的操作在外面,struct中只有操作。

  A way to organize: Designs and Implementations.

  对象才是Design和implementation首要关注的东西,而不是控制或数据流。

  To focus on things, not operations!

2.2 面向对象基本概念

  对象是互相之间在发送和接收消息。Objects send messages

Objects send messages!

  Messages are:

  •   composed by the sender
  •   interpreted by the receiver
  •   implemented by methods

  Messages:

  •   May cause receiver to change state
  •   May return results

  也就是说,不是在直接操作data,而是在改变operations!

Object VS. Class

  对象(实体)是东西,类(概念)是东西的种类。

  杯子是一个类,我手里的杯子是一个对象。

  OOP Characteristics:

  •   a. Everything is an object.
  •   b. A program is a bunch of objects telling each other what to do by sending messages.
  •   c. Each object has its own memory made up of other objects.
  •   d. Every object has a type.
  •   e. All objects of a particular type can receive the same messages

  每一个对象有其自己的内存和类型。

  一个特定类型的所有对象可接受相同的消息。所有可以接受相同消息的对象可以被认为是相同的类型。

  对象都具有接口,对象用接口接收消息,接口的功能有Communication和Protection(保护内部的东西)。

  OOP三大特性:封装,继承,多态

封装(Encapsulation)

  把数据和对数据所有的操作放在一起,操作在外,数据在内。

  隐藏数据和操作的细节。

  外界只能访问公开的部分

3 类

3.1 以售货机为例实现一个类

  每一个类都应该包含一个.h文件和一个.cpp文件。命名通常每个单词首字母大写。

TicketMachine.h :

#ifndef TICKETMACHINE_TICKETMACHINE_H
#define TICKETMACHINE_TICKETMACHINE_H


class TicketMachine {
public:
    TicketMachine();
    virtual ~TicketMachine();
    void showPrompt();
    void insertMoney(int money);
    void showBalance();
    void printTicket();
    void showTotal();
private:
    const int PRICE;
    int balance;
    int total;

};


#endif //TICKETMACHINE_TICKETMACHINE_H

TicketMachine.cpp:

#include "TicketMachine.h"
#include <iostream>

using namespace std;

TicketMachine::TicketMachine() : PRICE(0){

}

TicketMachine::~TicketMachine() {

}

void TicketMachine::showPrompt() {
    cout << "something";
}

void TicketMachine::insertMoney(int money) {
    balance += money;
}

void TicketMachine::showBalance() {
    cout << balance;
}

main.cpp:

#include <iostream>
#include "TicketMachine.h"

int main() {
    TicketMachine tm;
    tm.insertMoney(100);
    tm.showBalance();

    return 0;
}

  特别说明:

<Class Name>::<function name>
::<function name>

  ::的含义:域的解析符,::前有类的名字表示函数或变量属于这个类,如果没有东西则表示一个全局的函数或变量

void S::f(){
    ::f(); //全局的f()
    ::a++; //全局a
    a--; //类中的a
}

3.2 头文件

  在头文件中包含类的声明(Class declaration)和函数的原型(prototypes),函数内容(body)放在.cpp中。注意调用头文件内容时要先include该头文件。一个头文件只声明一个类。

  头文件 = 接口(interface)

  #include:编译预处理指令。

Declarations VS. Definitions

  •   一个.cpp就是一个编译单元
  •   只有declarations才能放在.h文件中:外部变量(extern variables); 函数原型(function prototypes);类和结构体的声明。

  #include “xx.h” :在当前目录寻找头文件。

  #include <xx.h> :在系统目录寻找头文件。

  #include :与第二种相同。

标准头文件结构

#ifndef 
#define

#endif

  使用这种结构是为了防止重复定义

3.3 用C++实现时钟

抽象(Abstract)

  Abstract is the ability to ignore details of parts to focus attention on a higher level of a problem.

3.4 成员变量

  三种变量:Fields(成员变量)、parameters(函数参数)、local variables(局部变量)

  •   All three kinds of variables are able to store a value that is appropriate to their defined type.

  •   Fields are defined outside construators and methods.

  •   Fields are used to store data that persists throughout the life of an object. As such, they maintain the current state of an object. They have a lifetime that lasts as long as their object class.

  •   Fields have class scope: their accessibility extends throughout the whole class, and so they can be used within any of the constructors or methods of the class in which they are defined.

  局部变量与全局变量命名重复,程序按就近原则处理。

  成员变量:在类的所有函数中可以直接使用,属于类的每一个对象。而函数则是属于类,不属于对象。

class A{
public:
    void f();
private:
    int i; // 这里的i是声明declaration而不是定义definition
};

void A::f(){
    int j = 10;

    cout << i << endl;
    i = 20; // A的i
    cout << i << endl;
}

int main(){
    A a; 
    a.i = 10;
    cout << i << endl;
    a.f();
    cout << i << endl;

    return 0;
}

  上述程序会输出:

10
10
20
20

成员变量的秘密

  当用不同对象调用同一个函数时,函数"知道"是哪个对象在调用它。

Point a;
a.print();

  There is a relationship with the function be called and the variable calls it.

  The function itself knows it is doing something with the variable.

this : the hidden parameter

  this : 所有的成员函数都具有的一个隐藏参数,类型是class。

  void Point::print() 可以写成 *void Point::print(Point p)

class A{
public:
    void f();
private:
    int i; 
};

void A::f(){
    this->i = 20;
    printf("A::f()--&i=%p\n", &i);
    printf("this=%p", this); //this 和 i 是一样的地址
}

  在成员函数中可以用this来指代变量。this不能被定义,但是可以直接使用。

3.5 构造与析构

  init变量很重要!构造函数用于确保init。

构造函数(constructor)

  构造函数名字与类相同,且没有返回类型,会在类被创建时自动被调用。构造函数也是一种成员函数。

构造函数与被调用的过程示意

  构造函数也可以有参数,参数传递方法是在构造对象时在后面加一个圆括号。

Tree(int i) {...}
Tree t(12);

析构函数(destructor)

  析构函数以类名称命名,并在前面加 ~ 符号。析构函数不含有参数。

class Y{
public:
    ~Y();
};

  析构意味着对象所占用的空间被收回,析构函数会在{}(scope)结束后被调用。

4 初始化

4.1 对象初始化

存储分配

  编译器会在{}开始就分配空间,但是构造函数是从定义对象开始的。

int main(){ //分配空间
    A a;
    a.i = 10;
    printf("&a = %p\n", &a);
    printf("&a.i = %p\n", &(a.i));
    A aa; //constructor happen
    a.f();
    printf("&aa = %p\n", &aa);
    aa.f();
}

  C++中struct和class某种程度上是一样的,struct也可以有构造函数。

default constructor

  A default constructor is one that can be called with no arguments. 默认构造函数无参数。

4.2 new & delete

动态内存分配

new int;
new int[10];
new stash //一个类

delete p; 
delete[] p; //new 带[]则delete带[]

  delete时,先析构,再收回空间。

动态数组

  int * psome = new int [10];:动态分配一块10个int类型数据大小的空间。

  delete [] psome:[]代表告诉程序delete整个数组大小的空间而不是只有第一个元素,psome所指的全部内容的析构都会被调用。无[]的话空间会被回收,但是析构只有一个会被调用。

int *a  = new int[10];

a++

delete [] a; // 会出错

在程序中对比 delete 和 delete[]

delete version :

  newdelete.h :

#ifndef NEW_DELETE_NEWDELETE_H
#define NEW_DELETE_NEWDELETE_H

#include <iostream>

using namespace std;

class A{
private:
    int i;
public:
    A();
    ~A();
   void set(int i);
   void f();
};

#endif //NEW_DELETE_NEWDELETE_H

  newdelete.cpp :

#include "newdelete.h"

A::A() {
    int i;
    cout << "A::A()" << endl;
}

A::~A() {
    cout << "A::~A(), i = "<< i << endl;
}

void A::set(int i) {
    this->i = i;
}

void A::f() {
    cout << "hello" << endl;
}

  main.cpp :

#include "newdelete.h"

int main() {
    int i;
    A* p = new A[10];

    for (i=0; i<10; i++){
        p[i].set(i);
    }

    delete p;

    return 0;
}

  程序输出为:

A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::~A(), i = 0

  new的顺序是反过来的,使用delete只会析构一次。

delete[] version :

  将main函数中的delete替换成delete[]

#include "newdelete.h"

int main() {
    int i;
    A* p = new A[10];

    for (i=0; i<10; i++){
        p[i].set(i);
    }

    delete[] p;

    return 0;
}

  输出结果如下:

A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::A()
A::~A(), i = 9
A::~A(), i = 8
A::~A(), i = 7
A::~A(), i = 6
A::~A(), i = 5
A::~A(), i = 4
A::~A(), i = 3
A::~A(), i = 2
A::~A(), i = 1
A::~A(), i = 0

Tips for new and delete

  •   不要用delete释放不是被new分配的空间。
  •   不要用delete重复释放同一块空间。
  •   delete[] 和 new[] 对应。
  •   delete 和 new 对应。
  •   可以用delete释放空指针(null pointer)。
int *p  = 0;

delete p  //可以编译通过

  这能简化代码编写,实际上,若在类中声明了一个指针并在构造函数将其定义为一个null pointer,那么析构函数有两种不同的写法。以下面的程序为例:

first version :

class A{
private:
    int *p;
public:
    A() { p = 0; cout << "hello"; }
    ~A() { delete p; cout << "end" << endl;}
    void f() { p = new int; }
};

  对于上述程序,当f()函数没有被调用过时,p是空指针,delete是safe的。当f()函数被调用过后,p不再是空指针,delete依然safe。

更加保险的Version :

class A{
private:
    int *p;
public:
    A() { p = 0; cout << "hello"; }
    ~A() { if ( p ) delete p; cout << "end" << endl;}
    void f() { p = new int; }
};

delete的意义

  对于循环的无法关闭的程序,如手机程序等等,避免内存泄露!

4.3 访问限制(Setting limits)

  这里解释为什么有 publicprivate 之分。

  To keep the client programmer’s hands off members they should not touch.

  To allow the library designer to change the internal workings of the structure without worring about how it will affect the client programmer.

  对于C++来说,所有的成员有如下三种访问属性:

  •   pubic:公开
  •   private:类的成员函数可以访问
  •   protected:类自己和其子类可以访问
#include <iostream>

using namespace std;

class A{
private:
    int i;
    int *p;
public:
    A() { p = 0; cout << "hello"; }
    ~A() { if ( p ) delete p; cout << "end" << endl;}
    void set(int ii) {i = ii;}
    void f() { p = new int; }
    void g(A* q) { cout << "A::g(), q->i = "<< i << endl;}
};

int main(){
    A* p = new A[10];
    for(i=0; i<10; i++>){
        p[i].set(i);
    }

    A b;
    b.set(100);
    p[0].g(&b);
    delete p;

    return 0;
}

  **prviate是对类来说的,而不是对对象来说的,同一个类的对象之间可以互相访问私有的成员变量。**因此上面的程序能够正常运行,将输出A::g(), q->i = 100

  C++的OOP特性只在源代码层面存在。只有在编译阶段才会检查是public还是private。

友元Friends

  声明一个“朋友”,这个“朋友”可以访问private属性的东西。

struct X;

struct Y{
    void f(X*);
};

struct X{
private:
    int i;
public:
    void initialize();
    friend void g(X*, int); //Global friend
    friend void Y::f(X*); //Struct member friend
    friend struct Z; //Entire struct is a friend
    friend void h();
};

void X::initialize() {
    i = 0;
}

void g(X* x, int i) {
    x->i = i;
}

void Y::f(X* x) {
    x->i = 57;
}

struct Z {
private:
    int j;
}

class VS. struct

  C++中classstruct唯一的区别在于当不声明private还是public时,class默认为privatestruct默认为public

4.4 初始化列表

class Point{
private:
    const float x, y;
    Point(float xa = 0.0, float ya = 0.0):y(ya), x(xa) {}
};

  x, y的初始化会早于构造函数执行。

Initialization(初始化) VS. assignment(赋值)

初始化:

Student::Student(string s):name(s) {}

赋值:

Student::Student(string s) {name = s;}

  这种方式先初始化了,再赋值。

  string must have a default constructor.

  类中的所有成员变量都用initialize list来做初始化!不要在构造函数里面做赋值!

5 软件重用

5.1 对象组合

  组合:用已有的对象来制造新的对象的过程.

  组合有两种方式:

  •   Fully:另一个对象是当前对象的一部分。(成员变量是对象本身)
  •   By reference:当前对象可以访问另一个对象,可以调用,但是它不属于当前对象的一部分。(成员变量是指针)

  形象的比喻,孩子在母亲肚子里面时是Fully,出生后则是By reference。

  从程序的角度来说,当设计一个“人”的对象,心脏应该是fully的(在里面)而书包一个是By reference的(在外面)。

Fully & By reference
class Person {...};
class Currency {...};
class SavingAccount {
public:
    SavingAccount(const char* name, const char* address, int cents);
    ~SavingAccount();
    void print();
private:
    Person m_saver; //Fully 自己初始化自己,自己管理自己
    Currency m_balance; //Fully
};

  m_saverm_balance要遵守原来的“规范”,自己初始化自己,自己管理自己。

SavingAccount::SavingAccount(const char* name, const char* address, int cents): m_saver(name, address), m_balance(0, cents) {}

void SavingAccount::print() {
    m_saver.print();
    m_balance.print();
}

  这里用了初始化列表的方式来初始化m_saverm_balance,使得二者的初始化在SavingAccount之前执行。如果不这样做,就要求PersonCurrency两个类有default constructor。

  Initialize list 就是用来做这件事情的,如果类中有成员变量里面有对象,那么它应该在initialize list中完成初始化。

Public VS. Private

  It is common to make embedded objects private:

  •   they are parts of the underlying implementation.
  •   the new class only has parts of the public interface of the old class.

  Can embed as a public object if you want to have the entire public interface of the subobject availaavaible in the new object.

class SavingAccount{
public:
    Person m_saver; // 假设Person类有set_name()函数
    ...
};

SavingAccount account;
account.m_saver.set_name("Fred");

5.2 继承(Inheritance)

  继承:用已有的类来改造得到一个新的类。继承也是软件重用的一种方式。

  继承和组合的区别在于,前者是用类来做出一个新的类,而后者则是用对象来制造一个新的对象。组合是“实”的,而继承是“虚”的。

  继承使得我们可以共享设计中的成员函数、成员变量和接口(interface 指一个类的public部分)。

   The ability to define the behaviour or implementation of one class as the superset (超集,数学概念)of another class.

“学生”继承“人,因为学生是人!

  注意,superclass指的是Person类,指“超类”,这是一个计算机概念。而superset指Student类,指“超集”,这是一个数学概念。

  继承一个类的时候,一定会对这个类进行扩充!

  当有两个存在继承关系的类时,通常画图时将被继承的类放在上面,继承的类放在下面。

程序表达:

class A{
    ...
};

class B : public A{ 
    ...
};

  B继承A,是A的子类。public是必需的!

  父类私有的变量是不能被继承的子类直接改变的。而如果父类中的某函数或变量是protected属性,那么子类可以对其进行操作。这里我的理解是想要操作protected中的东西,那操作者必需是class,而不能是定义的class类型的对象,这也解释了为什么视频中程序(下面写)b.set()是会报错的。protected属性的东西提供了一个接口,子类通过这个接口可以间接改变private属性的内容。

Inheritance.h:

#ifndef INHERITANCE_INHERITANCE_H
#define INHERITANCE_INHERITANCE_H

#include <iostream>

using namespace std;

class Father {
private:
    int i;
protected:
    void change(int j); // 提供了改变i的接口
public:
    Father();
    ~Father();
    void print();
    void set(int ii);
};

class Son : public Father {
public:
    void f();
};


#endif //INHERITANCE_INHERITANCE_H

Inheritance.cpp:

#include "Inheritance.h"

Father::Father() : i(0) {
    cout << "Father::Father()" << endl;
}

Father::~Father() {
    cout << "Father::~Father()" << endl;
}

void Father::set(int ii) {
    i = ii;
}

void Father::print() {
    cout << "Father::f(), i = " << i << endl;
}

void Father::change(int j) {
    i = j;
}

void Son::f() {
    set(100);
    print();
    change(200);
    print();
}

main.cpp:

#include "Inheritance.h"

int main() {

    Son s;
    s.f();
    // s.change(2); 这条语句会报错,因为对象s不能直接操作protected属性的内容。
    s.set(1);
    s.print();

    return 0;
}

  上述程序将输出:

Father::Father()
Father::f(), i = 100
Father::f(), i = 200
Father::f(), i = 1
Father::~Father()
Scopes and access in C++

5.3 子类父类关系

#include <iostream>

using namespace std;

class A{
private:
    int i;
public:
    A(int ii):i(ii) { cout << "A::A()" << endl; }
    ~A() { cout << "A::~A()" << endl; }
    void print() { cout << "A::f()" << i << endl; }
    void print(int i) {cout << i << endl; print();} //重载函数
    void set(int ii) { i = ii; }
};

class B : public A {
public:
    B() : A(15) { cout << "B::B()" << endl; } //这里,必须对A的构造函数初始化
    ~B() { cout << "B::~B()" << endl; }
    void print() { cout << "B::print()" << endl; }
    void f() {
        set(20);
        print();
    }
};

int main(){
    B b;
    b.set(10);
    b.print();
}

  父类构造函数调用,放在子类构造函数的initialize list里。

  基类通常会被先构造。

  如果没有明确的参数传递给基类,则default constructor会被调用。

  析构的顺序和构造的顺序相反。

  父类当中有重载函数,子类中出现和父类重复的函数(参数、名字都一样),则子类只有自己的这个函数,父类的那个函数就被隐藏掉了。(比较拗口!),以上面的程序为例说明此事:

int main(){
    b.print(20); // 会报错
}
报错内容:no mathing function for call to ...

  如果想调用A类中的print(int i)函数,则需如下写法:

int main(){
    b.A::print(20); 
}

  以程序为进一步说明:

Inheritance.h:

#ifndef INHERITANCE_INHERITANCE_H
#define INHERITANCE_INHERITANCE_H

#include <iostream>

using namespace std;

class Father {
private:
    int i;
protected:
    void change(int j);
public:
    Father();
    ~Father();
    void print();
    void print(int i);
    void set(int ii);
};

class Son : public Father {
public:
    void f();
    void print();
};

#endif //INHERITANCE_INHERITANCE_H

Inheritance.cpp:

//
// Created by PC on 2023-07-30.
//

#include "Inheritance.h"

Father::Father() : i(0) {
    cout << "Father::Father()" << endl;
}

Father::~Father() {
    cout << "Father::~Father()" << endl;
}

void Father::set(int ii) {
    i = ii;
}

void Father::print() {
    cout << "Father::f(), i = " << i << endl;
}

void Father::print(int i) {
    cout << i << endl;
    print();
}

void Father::change(int j) {
    i = j;
}

void Son::f() {
    set(100);
    print();
    change(200);
    print();
}

void Son::print() {
    cout << "Son::print()" << endl;
}

main.cpp:

#include "Inheritance.h"

int main() {

    Son s;
    s.f();
    s.set(1);
    s.print();

    s.Father::print(20);

    return 0;
}

  输出:

Son::print() //其实这里也说明了,前面输出的是Father::f(), i = 100,这里变成了子类的print,说明父类的被隐藏了。若要调用父类中的print函数也是要在前面加上::域的解析符。
Son::print()
Son::print()
20
Father::f(), i = 1
Father::~Father()

6 函数

6.1 函数重载和默认参数(缺省参数值)

  两个函数名称相同,参数表相同,返回类型不同不能重载。重载一定是参数表不同。

  Default arguments:A default arguments is a value given in the declaration that the complier automatically inserts if you don not provide a value in the function call.

Stash(int size, int initQuantity = 0);

  To define a function with an argument list, defaults must be added from right to left. 必须从右到左。

int f(int i, int j = 1, int m = 2);
int g(int i, int j = 1, int m ); // illeagle
int h(int i = 0, int j = 1, int m = 2)

beeps = f(2); // i = 2
beeps = f(1, 8); // i = 1, j = 8
beeps  = f(5, 6, 7); // i = 5, j = 6, m = 7 

  Default arguments must be written in .h file but not in .cpp file.

  建议不要使用default value!

6.2 内联函数

  调用函数时有一些overhead(额外的开销)。

  inline function 内联函数能够帮助减少overhead,提高成运行速度。与常规函数的区别在于内联函数的编译代码与其他程序代码“内联”起来了。编译器会使用相应的函数代码替换函数调用,也就是说,内联代码程序无序调到另一处执行代码再跳回来。因此,内联函数的运行速度比常规函数快。这是从汇编语言的层面上的理解方式!

  内联函数需要占用更多的内存,如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。(C++ primer plus)

  Repeat inline keyword at declaration and definition. 即,头文件和cpp文件都要写 inline 关键字

  An inline function definition may not generate any code in .obj file.

  Never be afraid of multi-definition of inline functions, since they have no body at all.

  Definitions of inline functions are just declarations. 在函数前面加了inline以后,就不再是定义而是声明了!所以应该将内联函数的body放在头文件中,当需要调用这个函数时#include即可。

  inline函数实际上是一种以空间换时间的策略。inline函数与C中的宏类似,但是宏是不可以做类型检查的,因此inline安全。

Inline may not in-line

  编译器有可能会拒绝inline。如果函数过大或者是递归的,那么编译器可以拒绝。

Inline inside classes

  Any function you define inside a class declaration is automatically an inline.

  在类中将函数body写在函数后面就是inline!

class Rectangle {
    int width, height;
public:
    Rectangle ( int w = 0, int h = 0 ); // inline的写法
    int getWidth() const;
    void setWidth(int w);
    int getHeight() const;
    void setHeight(int h);
}

inline Rectangle::Rectangle(int w, int h) : width(w), height(h) {}

inline Rectangle::getWidth() const {
    return width;
}

  在类的下面写inline的声明,使得代码简洁。

Inline or not?

  Inline:

  •   Small functions, 2 or 3 lines.
  •   Frequently called functions, e.g. inside loops

  Not inline:

  •   Very large functions, more than 20 lines.
  •   Recursive functions.(递归函数)

6.3 Const

  const变量依然是变量,而不是常数。

  const int bufsize = 1024;

  •   value must be initialized
  •   unless you make an explicit extern declaration

  extern const int bufsize;

  •   compiler will not let you change it
const int class_size = 12;
int finalGrade[class_size]; //OK

int x;
cin >> x;
const int size = x;
double classAverage[size]; // error!因为编译的时候不知道x

Pointers and const

  A pointer may be const. Meanwhile, a value may also be a const.

char * const q = "abc"; // q is const
*q = 'c'; //OK
q++; // error!
const char *p = "ABCD";
// (*p) is a const char
*p = 'b'; // Error! (*p) is the const 

  const 在*号前就是对象是const。

  const 在*号后就是指针是const。不能通过指针修改对象,但是不意味着对象是const的了!

const对象和指针

  

  上图中为什么cip = &ci是可行的?因为cip保证了通过*cip不能修改指向对象的内容!

char s2[] = "hello world";

char *s1 = "hello world"; //等价于const char *s1 = "hello world";

  s1在代码段中,不可修改,s2在堆栈中,可以修改。

Passing and returning address !

  Passing a whole object may cost you a lot. It is better to pass by a pointer. But it is possible for the programmer to take it and modify the original value.

  In fact, whenever you are passing an address into a function, you should make it a const if at all possible!

Constant objects & Const member functions

  对象是const的:

const Currency cur(1, 2);

  函数是const的:

int Date::get_day() const {
    day++; // error!
    set_day(12); // error!

    return day;
}

  函数是const的意味着该函数不修改任何成员变量。

Const member function usage

  Repeat the const keyword in the definition as well as the declaration.

int get_day() const;

int get_dat() const {return day};

  Function members that do not modify data should be declared const.

  const member functions are safe for const objects!

  this是const的!

class A {
    int i;
public:
    A : i(0) {}
    void f() {cout << "f()" << endl;}
    void f() const {cout << "f() const" << endl;}
};

int main(){
    const A a;
    a.f();

    return 0;
}

  两个f()函数能构成overload的本质原因是:

void f(A* this)
void f(const A* this)

  调用的时候因为a是const,所以会选择const的函数调用。

  成员变量如果是const的,那么必须在initilaizer list中将其初始化。

6.4 引用

  通过引用来访问一个对象。

  References are a new type in C++.

char& r = c; //引用

  全局变量或局部变量初始化时必须赋值,成员变量则不需要,会在构造函数时初始化初值。

  引用是声明了一个新的name给已存在的变量。

int X = 47;
int& Y  = X; // Y is a reference to X

// X 和 Y 现在是同一个变量

cout << "Y  = " << Y; // Y = 47
Y = 18;
cout << "X  = " << X; // X = 18

引用的规则

  • References must be initialized when defined.
  • Initialization establishes a binding.

  声明中:

int x = 3;
int& y = x;
const int& z = x; //通过z不能修改x

  函数参数中:

void f( int& x );
f(y); // initialized when function is called
  • Bindigs do not change at run time, unlike pointers.
  • Assignment changes the object referred-to.
int& y = x;
y = 12; // changes value of x
  • The target of a reference must have a location
void func(int &);
func ( i*3 ); //Warning or error! i*3 has no location!

Poninters VS. References

  实际上,引用就是const pointers,设计引用的目的就是减少*号。

引用和指针的区别

  引用类型不能互相赋值。

限制

  No references to references.

  No pointers to refereneces.

int&* p; //illegal 不能取得reference的地址

  Reference to pointer is ok.

void f(int*& p); 

  p是一个引用,它所bind的变量是一个指针。

  No arrays of references.

6.5 向上造型(upcast)

  子类对象被当做父类看待,同样还有向下造型(downcast)。

Manager pete("Pete", "111-111-111","Bakery");
Employee* ep = &pete; //Upcast
Employee& er = pete; //Upcast

6.6 多态性(Polymorphism)

  virtual函数在通过指针或引用调用时不能确定是哪个,只有给了参数以后才能够确定,这里的参数就具有多态性,即指的是谁,就变成谁的形态!

  多态性建立在两个东西的基础上,一个是upcast,另一个是dynamic binding。

  • Upcast: take an object of the derived class as an object of the base one.
  • Dynamic binding: binding——which function to be called. Call the function of the object. 运行时才知道调用的是哪个函数。(Static binding: call the function as the code. —— (编译时就知道调用是哪个函数)

6.7 多态的实现

  任何一个类如果有虚函数,这个类的对象就会比正常的大一点。

#include <iostream>
using namspace std;

class A {
public:
    A() : i(10) {}
    virtual void f() {cout << "A::f()" << i << endl; }

    int i;
};

int main(){
    A a;
    a.f();
    cout << sizeof(a) << endl;

    int *p = (int*)&a;
    p++;
    cout << *p << endl; //输出为10

    return 0;
}

  所有有virtual的类的对象在头上会有一个隐藏的vptr指针,指向存放所有virtual函数地址的vtable表。

vptr指针指向vtable
#include <iostream>
using namspace std;

class A {
public:
    A() : i(10) {}
    virtual void f() {cout << "A::f()" << i << endl; }

    int i;
};

class B : public A {
public:
    B() : j(20) {}
    virtual void f() { cout << "B::f()" << j << endl;}

    int j;
};

int main(){
    A a;
    B b;

    A* p = &b;
    
    p->f(); // 执行的是b的f()

    a = b;

    a.f(); // 执行的是a的f()

    p = &a;
    p->f(); // 执行的是a的f()

    return 0;
}

  在a = b的过程中,vptr不传递。

#include <iostream>
using namspace std;

class A {
public:
    A() : i(10) {}
    virtual void f() {cout << "A::f()" << i << endl; }

    int i;
};

class B : public A {
public:
    B() : j(20) {}
    virtual void f() { cout << "B::f()" << j << endl;}

    int j;
};

int main(){
    A a;
    B b;

    A* p = &a;

    int* r = (int*)&a;
    int* t = (int*)&b;

    *r = *t;
    
    p->f(); // 执行的是b的f()

    return 0;
}

  改变了vptr

  如果类中有一个virtual函数,则析构函数必须是virtual的!

  Overide:父类和子类的两个函数是virtual的,且名称相同参数也相同,就构成overiding关系(覆盖或改写)。在这种关系下,想调用父类的函数,需要加Bass::作用域。

  如果DB的子类,D::f()能够返回B::f()的指针和引用类型。

返回对象本身错误

  父类里面有virtual的两个overloaded的函数,在子类里面必须override所有的virtual函数,否则会发生name hidding。

6.8 引用再研究

References as class members

  Declared without initial value.

  Must be initialized using constructor initializer list.

class X {
public:
    int& m_y;
    X(int& a);
};

X::X(int& a) : m_y(a) {}

Returning references

  Functions can return references. But they better refer to non-local variables!

  返回引用时,返回的是一个直接的变量(值)。

  以程序为例说明:

#include <iostream>
using namespace;

double myarray[10] = {0};

double& subscript (int i){
    return myarray[i]; // 函数类型是引用,返回的实际上是数组的值
}

int main() {
    double value = subscript(12);
    subscript(3) = 12.5;
}

Const in Functions Arguments

  使用指针或引用是常用的对象参数传递方式,并在前面加上const,保证函数不能修改该对象。引用是一种更好的用法,好处是省略了*号。

person(const string& name, int weight);

Const in Function returns

  返回一个const的value

#include <iostream>
using namespace std;

class A {
public:
    int i;
    A() : i(0) {}    
};

A f (){
    A a;
    return a;
}

int main(){
    f().i = 10;

    A b;
    b.i = 20;
    f() = b;

    return 0;
}

  f()函数返回的是一个对象,所以可以做左值。如果是const A f()则不能做左值,因为返回的对象是const的。

6.9 拷贝构造——The copy constructor

  拷贝构造:

T::T(const T&);

  做成员变量对成员变量的拷贝——成员级别的拷贝,而不是字节对字节的拷贝。

  下面都是初始化initialization

Person baby_a("A");
// these use the copy ctor

Person baby_b = baby_a; //not an assignment
Person baby_c(baby_a); //not an assignment

  编译器有时会优化“冗余”的拷贝。

Person copy_func(char *who) {
    Person local(who);
    local.print();
    return local; // 此时copy ctor called
}

Person nocopy_func(char *who) {
    return Person(who); // 此时no copy needed!,因为和Person无关
}

  在C++中要使用string类。

  一旦写了一个类,一定要给它三个函数:

  •   default constructor
  •   virtual destructor
  •   copy constructor

  关于拷贝构造,我觉得黑马程序员的课程讲解更通俗一些,因此在这里放上他们课程的笔记:

黑马程序员课程关于拷贝构造的笔记

拷贝构造函数调用时机

  C++中拷贝构造函数调用时机通常有三种情况

  •   使用一个已经创建完毕的对象来初始化一个新对象
  •   值传递的方式给函数参数传值
  •   以值方式返回局部对象

  示例:

class Person {
public:
	Person() {
		cout << "无参构造函数!" << endl;
		mAge = 0;
	}
	Person(int age) {
		cout << "有参构造函数!" << endl;
		mAge = age;
	}
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		mAge = p.mAge;
	}
	//析构函数在释放内存之前调用
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

	Person man(100); //p对象已经创建完毕
	Person newman(man); //调用拷贝构造函数
	Person newman2 = man; //拷贝构造

	//Person newman3;
	//newman3 = man; //不是调用拷贝构造函数,赋值操作
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
	Person p; //无参构造函数
	doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2()
{
	Person p1;
	cout << (int *)&p1 << endl;
	return p1;
}

void test03()
{
	Person p = doWork2();
	cout << (int *)&p << endl;
}


int main() {

	//test01();
	//test02();
	test03();

	system("pause");

	return 0;
}
构造函数调用规则

  默认情况下,c++编译器至少给一个类添加3个函数

  1.默认构造函数(无参,函数体为空)

  2.默认析构函数(无参,函数体为空)

  3.默认拷贝构造函数,对属性进行值拷贝

  构造函数调用规则如下:

  •   如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
  •   如果用户定义拷贝构造函数,c++不会再提供其他构造函数

  示例:

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	Person(const Person& p) {
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int age;
};

void test01()
{
	Person p1(18);
	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	Person p2(p1);

	cout << "p2的年龄为: " << p2.age << endl;
}

void test02()
{
	//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
	Person p1; //此时如果用户自己没有提供默认构造,会出错
	Person p2(10); //用户提供的有参
	Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

	//如果用户提供拷贝构造,编译器不会提供其他构造函数
	Person p4; //此时如果用户自己没有提供默认构造,会出错
	Person p5(10); //此时如果用户自己没有提供有参,会出错
	Person p6(p5); //用户自己提供拷贝构造
}

int main() {

	test01();

	system("pause");

	return 0;
}
深拷贝与浅拷贝

  深浅拷贝是面试经典问题,也是常见的一个坑

  浅拷贝:简单的赋值拷贝操作

  深拷贝:在堆区重新申请空间,进行拷贝操作

  示例:

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int age ,int height) {
		
		cout << "有参构造函数!" << endl;

		m_age = age;
		m_height = new int(height);
		
	}
	//拷贝构造函数  
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_age = p.m_age;
		m_height = new int(*p.m_height);
		
	}

	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
		if (m_height != NULL)
		{
			delete m_height;
		}
	}
public:
	int m_age;
	int* m_height;
};

void test01()
{
	Person p1(18, 180);

	Person p2(p1);

	cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;

	cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

  总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

6.10 静态对象

  Static member variables: Shared by all instances.

  Static member function: Shared by all instances, can only access static member variables.

  当函数中有一个static的本地变量时,这个static的local variable 就是全局变量。

6.11 静态成员

  Static means: Hidden and Persistent. Persistent 意味着static成员变量或函数不依赖于对象的存在而存在,是共享的。

#include <iostream>
using namespace std;

class A {
public: 
    A() { i = 0 };
    void print() {cout << i << endl;}
    void set(int ii) {i = ii};
    static int j;
    static say(int k) {cout << k << i << endl;}
private:
    static int i; //declaration声明,而不是definition
};

int A::i; // 有static的成员变量必须在某一个cpp文件里面写这样一句话来定义这个static成员变量
int A::j = 10;

int main(){
    A a,b;
    a.set(10);
    b.print();

    a.say(0);
    A::say(0);//两种方式调用public属性的静态成员函数

    cout << a.j << endl;
    cout << A::j << endl; //两种方式访问public属性的静态成员变量

    return 0;
}

  有static的成员变量必须在某一个cpp文件里面定义这个static成员变量: int A::i;, 而不能在initialize list中初始化。

  两种方式访问public属性的静态成员变量:

    cout << a.j << endl;
    cout << A::j << endl;

  两种方式调用public属性的静态成员函数:

    a.say(0);
    A::say(0);

  静态函数只能调用静态成员函数,访问静态成员变量,原因如下:

  静态成员变量没有隐藏参数this。因此上面的程序不能写this->i。

7 运算符重载 —— Overloading Operators

7.1 基本规则

  写函数改变运算符的行为,可以不使用默认运算功能,而使用重新定义过的功能进行运算。

 &emsp可以能重载的运算符如下:

可以重载的运算符

  不能重载的运算符如下:

不可以重载的运算符

限制

  •   只有已经存在的运算符可以重载。
  •   只能对一个类或枚举类型来重载运算符。
  •   运算符重载必须保证原有操作数的个数和优先级。

  Use the operator keyword as a prefix to name

operator *(...)

  可以作为成员函数,已有hidden parameter —— this

const String String::operator +(const String& that); 

  可以作为全局函数

const String operator +(const String& r, const String& l); 

  As member function:

  •   this 是它的第一个参数
  •   No type conversion performed on recevier
  •   Must have access to class definition
class Integer {
public:
    Integer(int n = 0) : i(n) {}
    const Integer operator+(const Integer& n) const{
        return Integer(i + n.i)
    }
private:
    int i;
};

  如何使用:

Integer x(1), y(5), z;
x + y; 

  这里的x + y 实际上是x.operator+(y)。从而我们引出了一个新的概念——receiver。运算符的左边是receiver。

  前面所说的No type conversion performed on recevier的意思是:receiver决定了operator用哪个,当receiver是Integer时,就是使用重载的+号。

z = x + y; //可行
z = x + 3; //可行
z = 3 + y; //不可行

  z = x + 3可行是因为,当receiver是x,使用的加法是重载后的+,3可以被转换成一个Integer类型的对象。而z = 3 + y不可行是因为使用的是正常的+。

Operator as a global function

const Integer operator+(const Integer& rhs, const Integer& lhs);

Integer x, y;

  对private属性的成员变量进行运算时需要在类中被声明为友元 friend

  Type conversions performed on both arguments.

class Integer {
    friend const Integer operator+(const Integer& rhs, const Integer& lhs);
}

const Integer operator+(const Integer& rhs, const Integer& lhs) {
    return Integer(lhs.i + rhs.i);
}

  此时,z = 3 + y可行。

  •   Unary oprators(单目运算符) should be members.
  •   = () -> ->* must be memberss.
  •   assignment operators should be members.
  •   All other binary operators(双目运算符) as non-members.

7.2 原型

参数传递——Argument Passing

  运算符不修改算子的(read-only),传递参数时要加const,以reference的形式传递。而像++、–这样的算子则可以不加const。

  成员函数不能修改类,要声明const。Make member functions const that do not change the class (boolean operators, +, -, etc).

  全局函数可能两个都是const,或者有一个不加。

返回值——Return Values

  决定返回的东西是对自己修改还是制造了一个新东西。

  For operator+ you need to generate a new object. Return as a const object so the result can not be modified as an Ivalue.

  Logical operators should return bool (or int for older compilers).

The prototypes of operators

  常见运算符:

++、--运算符重载

  prefix代表++a,postfix代表a++,二者通过后面有无int参数进行区分。

const Integer& Integer::operator++() {
    *this += 1;
    return *this;
}

const Integer Integer::operator(int) {
    Integer old(*this); //拷贝构造
    ++(*this); //用上面的++来定义这个++
    return old;
}

  关系运算符:

关系运算符
关系运算符

  可以看出:

  • implement != in terms of ==.
  • implement>, >=, <=, in terms of <.

  实际上只定义了==和<两个原函数,好处是如果要改变行为只要改两个就行了

  index运算符[]:

  必须是成员函数,且只有单一参数。

  返回引用类型:

Vector v(100);  //create a vector of size 100
v[10] = 45;

  返回指针类型:

*v[10] = 45;

7.3 赋值

class Fi {
public:
    Fi() {}
};

class Fee {
public:
    Fee(int) {}
    Fee(const Fi&) {}
};

int main() {
    Fee fee = 1; // Fee(int)
    Fi fi;
    Fee fum = fi; // Fee(Fi)
    fum = fi; //先构造一个Fee的对象,再赋值给fum
}

Assignment operator

  Must be a member function.

  Check for assignment to self.对自己做赋值。

  返回一个*this。

A = B = C;
// executed as
A = (B = C);
T& T::operator=(const T& rhs) {
    if (this != &rhs) {
        // perform assignment
    } //这里是必须的,看起来无用,但是实际上有用,在下面的程序段中说明
    return *this;
}
class A {
    char *p;
    A& operator=(cosnt A& that){
        delete p;

        p = new [strlen(that.p)+1];

        strcpy(p, that.p);

        return *this;
    }
}

  在上面的类中,如果that就是this,那么p会被先delete掉,从而后面的that.p毫无意义。

  For classes with dynamically allocated memory declare an assignment operator (and a copy constructor).

  To prevent assignment, explicitly declare operator= as private.

7.4 类型转换

  Value class: 如复数complex,字符串string等。这种类与原来的数据类型相似,能被传递或作为返回类型,通常有重载操作符,且能够由其他类型转换得到或被转换成其他类型。

class one {
public:
    one() {}
};

class two {
    string name
public:
    two (const one&) {}
    explicit three (cosnt string&) {}
    ~two();
};

void f(two) {}

int main() {
    one one, one1;
    f(one);

    string abc("abc");
    two xyz(abc); // OK!
    xyz = abc; // error!
}

  最后得f(one)看似类型不匹配,但是实际上发生的是先调用构造函数将one转换成two再把这个two类型的参数给f()。

  若要防止这种自动的隐藏的类型转换,需要在成员函数前加explicit关键字。

  也可以写转换操作符Conversion operations:

转换操作符

  没有返回类型,因为double()已经可以知道它的返回类型了。

  通用形式:

X::operator T()

  T代表操作符名字,可以是任意类型。

  没有参数,没有返回类型,编译器会用这个操作符将X转换成T。

  两种类型转换方法:

两种类型转换方法

  尽量不要使用这种自动的方式,虽然可以用explicit控制。还是自己写一个比较好。

8 模板——template

  模板允许我们重用代码,类型成为了参数。Perform similar operations on different types of data.

  函数模板:

template <class T>
void swap(T& x, T& y) {
    T temp = x;
    x = y;
    y = temp;
}

  template是declaration而不是definition。

  模板必须要确定出T的数据类型,才可以使用

template<class T>
void func()
{
	cout << "func 调用" << endl;
}

void test02()
{
	//func(); //错误,模板不能独立使用,必须确定出T的类型
	func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板
}

  类模板里面的每一个函数都是函数模板,并且在这些函数中间类的名字后面也要加上<>。

  Example: Vector

template <class T>
class Vector{
public:
    Vector(int);
    ~Vector();
    Vector(const Vector&);
    Vector& operator[] (int);
private:
    T* m_elements;
    int m_size;
};

template <class T>
Vector<T>::Vector(int siaze) : m_size(size) {
    m_elements = new T[m_size];
}

template <class T>
T& Vector<T>::operator[] (int indx) {
    if (indx < m_size && indx > 0) {
        return m_elements[indx];
    }
    else {...}
}

  模板可以有多个参数。可以用单个大写字母避免误解。

template<class K, class V>
class HashTable {
    const V& lookup(const K&) const;
    void install(cosnt K&, const V);
};

  注意空格:

Vector< Vector< double *> >

  参数可以很复杂:

Vector< int (*)(Vector< double >&, int) >  // 函数指针

  模板参数可以是常量形式,如果不指定常量的大小,则使用默认的大小。

template < class T, int bounds = 100 >
class FixedVector {
public:
    FixedVector();
    // ...
    T& operator[](int);
private:
    T elements[bounds];
};

// Usage:
FixedVector<int, 50> v1;
FixedVector<int, 5*10> v2;
FixedVector<int> v1; //使用默认值100

  模板与继承三种情况:

模板与继承的三种情况

9 泛型编程

9.1 模板

9.1.1 模板的概念

  模板就是建立通用的模具,大大提高复用性

  模板不可以直接使用,只是一个通用框架。

9.1.2 函数模板

  C++提供两种模板机制:函数模板类模板

  函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

template<typename T> // typename可以替换成class

  两种方式:

  • 自动类型推导
  • 显示指定类型 mySwap<int>(a, b);

  注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用。
  • 模板必须要确定出T的数据类型,才可以使用。
//利用模板提供通用的交换函数
template<class T>
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}


// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{
	int a = 10;
	int b = 20;
	char c = 'c';

	mySwap(a, b); // 正确,可以推导出一致的T
	//mySwap(a, c); // 错误,推导不出一致的T类型
}


// 2、模板必须要确定出T的数据类型,才可以使用
template<class T>
void func()
{
	cout << "func 调用" << endl;
}

void test02()
{
	//func(); //错误,模板不能独立使用,必须确定出T的类型
	func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板
}

int main() {

	test01();
	test02();

	system("pause");

	return 0;
}

  使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型。

  普通函数与函数模板区别:

  •   普通函数调用时可以发生自动类型转换(隐式类型转换)。
  •   函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。
  •   如果利用显示指定类型的方式,可以发生隐式类型转换。

  建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T。

  普通函数和函数模板可以发生重载,调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数。

  2. 可以通过空模板参数列表来强制调用函数模板。

  3. 函数模板也可以发生函数重载。

  4. 如果函数模板可以产生更好的匹配,优先调用函数模板。

//普通函数与函数模板调用规则
void myPrint(int a, int b)
{
	cout << "调用的普通函数" << endl;
}

template<typename T>
void myPrint(T a, T b) 
{ 
	cout << "调用的模板" << endl;
}

template<typename T>
void myPrint(T a, T b, T c) 
{ 
	cout << "调用重载的模板" << endl; 
}

void test01()
{
	//1、如果函数模板和普通函数都可以实现,优先调用普通函数
	// 注意 如果告诉编译器  普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
	int a = 10;
	int b = 20;
	myPrint(a, b); //调用普通函数

	//2、可以通过空模板参数列表来强制调用函数模板
	myPrint<>(a, b); //调用函数模板

	//3、函数模板也可以发生重载
	int c = 30;
	myPrint(a, b, c); //调用重载的函数模板

	//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
	char c1 = 'a';
	char c2 = 'b';
	myPrint(c1, c2); //调用函数模板
}

int main() {

	test01();

	system("pause");

	return 0;
}

  提供了函数模板,最好就不要提供普通函数,否则容易出现二义性!

  模板也具有一定的局限性。

#include<iostream>
using namespace std;

#include <string>

class Person
{
public:
	Person(string name, int age)
	{
		this->m_Name = name;
		this->m_Age = age;
	}
	string m_Name;
	int m_Age;
};

//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
	if (a == b)
	{
		return true;
	}
	else
	{
		return false;
	}
}


//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
	if ( p1.m_Name  == p2.m_Name && p1.m_Age == p2.m_Age)
	{
		return true;
	}
	else
	{
		return false;
	}
}

void test01()
{
	int a = 10;
	int b = 20;
	//内置数据类型可以直接使用通用的函数模板
	bool ret = myCompare(a, b);
	if (ret)
	{
		cout << "a == b " << endl;
	}
	else
	{
		cout << "a != b " << endl;
	}
}

void test02()
{
	Person p1("Tom", 10);
	Person p2("Tom", 10);
	//自定义数据类型,不会调用普通的函数模板
	//可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
	bool ret = myCompare(p1, p2);
	if (ret)
	{
		cout << "p1 == p2 " << endl;
	}
	else
	{
		cout << "p1 != p2 " << endl;
	}
}

int main() {

	test01();

	test02();

	system("pause");

	return 0;
}

  总结:

  •   利用具体化的模板,可以解决自定义类型的通用化
  •   学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

9.1.3 类模板

template<class T>
class ...
#include <string>
//类模板
template<class NameType, class AgeType> 
class Person
{
public:
	Person(NameType name, AgeType age)
	{
		this->mName = name;
		this->mAge = age;
	}
	void showPerson()
	{
		cout << "name: " << this->mName << " age: " << this->mAge << endl;
	}
public:
	NameType mName;
	AgeType mAge;
};

void test01()
{
	// 指定NameType 为string类型,AgeType 为 int类型
	Person<string, int>P1("孙悟空", 999);
	P1.showPerson();
}

int main() {

	test01();

	system("pause");

	return 0;
}

  类模板与函数模板区别主要有两点:

  •   类模板没有自动类型推导的使用方式。
  •   类模板在模板参数列表中可以有默认参数。
#include <string>
//类模板
template<class NameType, class AgeType = int> 
class Person
{
public:
	Person(NameType name, AgeType age)
	{
		this->mName = name;
		this->mAge = age;
	}
	void showPerson()
	{
		cout << "name: " << this->mName << " age: " << this->mAge << endl;
	}
public:
	NameType mName;
	AgeType mAge;
};

//1、类模板没有自动类型推导的使用方式
void test01()
{
	// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
	Person <string ,int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
	p.showPerson();
}

//2、类模板在模板参数列表中可以有默认参数
void test02()
{
	Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
	p.showPerson();
}

int main() {

	test01();

	test02();

	system("pause");

	return 0;
}

  类模板中成员函数和普通类中成员函数创建时机是有区别的:

  •   普通类中的成员函数一开始就可以创建。
  •   类模板中的成员函数在调用时才创建。
class Person1
{
public:
	void showPerson1()
	{
		cout << "Person1 show" << endl;
	}
};

class Person2
{
public:
	void showPerson2()
	{
		cout << "Person2 show" << endl;
	}
};

template<class T>
class MyClass
{
public:
	T obj;

	//类模板中的成员函数,并不是一开始就创建的,而是在模板调用时再生成

	void fun1() { obj.showPerson1(); }
	void fun2() { obj.showPerson2(); }

};

void test01()
{
	MyClass<Person1> m;
	
	m.fun1();

	//m.fun2();//编译会出错,说明函数调用才会去创建成员函数
}

int main() {

	test01();

	system("pause");

	return 0;
}

  即:类模板中的成员函数并不是一开始就创建的,在调用时才去创建。

  类模板对象做函数参数一共有三种传入方式:

  1. 指定传入的类型:直接显示对象的数据类型

  2. 参数模板化:将对象中的参数变为模板进行传递

  3. 整个类模板化:将这个对象类型 模板化进行传递

#include <string>
//类模板
template<class NameType, class AgeType = int> 
class Person
{
public:
	Person(NameType name, AgeType age)
	{
		this->mName = name;
		this->mAge = age;
	}
	void showPerson()
	{
		cout << "name: " << this->mName << " age: " << this->mAge << endl;
	}
public:
	NameType mName;
	AgeType mAge;
};

//1、指定传入的类型
void printPerson1(Person<string, int> &p) 
{
	p.showPerson();
}
void test01()
{
	Person <string, int >p("孙悟空", 100);
	printPerson1(p);
}

//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{
	p.showPerson();
	cout << "T1的类型为: " << typeid(T1).name() << endl;
	cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
	Person <string, int >p("猪八戒", 90);
	printPerson2(p);
}

//3、整个类模板化
template<class T>
void printPerson3(T & p)
{
	cout << "T的类型为: " << typeid(T).name() << endl;
	p.showPerson();

}
void test03()
{
	Person <string, int >p("唐僧", 30);
	printPerson3(p);
}

int main() {

	test01();
	test02();
	test03();

	system("pause");

	return 0;
}

  最常用的是直接指定传入模型。

  当类模板碰到继承时,需要注意一下几点:

  •   当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型。
  •   如果不指定,编译器无法给子类分配内存。
  •   如果想灵活指定出父类中T的类型,子类也需变为类模板。
template<class T>
class Base
{
	T m;
};

//class Son:public Base  //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> //必须指定一个类型
{
};
void test01()
{
	Son c;
}

//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2>
{
public:
	Son2()
	{
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
	}
};

void test02()
{
	Son2<int, char> child1;
}


int main() {

	test01();

	test02();

	system("pause");

	return 0;
}

  类模板成员函数类外实现:类模板中成员函数类外实现时,需要加上模板参数列表

#include <string>

//类模板中成员函数类外实现
template<class T1, class T2>
class Person {
public:
	//成员函数类内声明
	Person(T1 name, T2 age);
	void showPerson();

public:
	T1 m_Name;
	T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

void test01()
{
	Person<string, int> p("Tom", 20);
	p.showPerson();
}

int main() {

	test01();

	system("pause");

	return 0;
}

  类模板分文件编写:

  类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到!所以当分文件编写类模板文件时不能直接包含头文件。

  •   解决方式1:直接包含.cpp源文件。
  •   解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制。

person.hpp中代码:

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

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
public:
	T1 m_Name;
	T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

类模板分文件编写.cpp中代码

#include<iostream>
using namespace std;

//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件

//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
	Person<string, int> p("Tom", 10);
	p.showPerson();
}

int main() {

	test01();

	system("pause");

	return 0;
}

  类模板与友元:

  全局函数类内实现:直接在类内声明友元即可。

  全局函数类外实现:需要提前让编译器知道全局函数的存在。

#include <string>

//2、全局函数配合友元  类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template<class T1, class T2> class Person;

//如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
//template<class T1, class T2> void printPerson2(Person<T1, T2> & p); 

template<class T1, class T2>
void printPerson2(Person<T1, T2> & p)
{
	cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}

template<class T1, class T2>
class Person
{
	//1、全局函数配合友元   类内实现
	friend void printPerson(Person<T1, T2> & p)
	{
		cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
	}


	//全局函数配合友元  类外实现
	friend void printPerson2<>(Person<T1, T2> & p);

public:

	Person(T1 name, T2 age)
	{
		this->m_Name = name;
		this->m_Age = age;
	}


private:
	T1 m_Name;
	T2 m_Age;

};

//1、全局函数在类内实现
void test01()
{
	Person <string, int >p("Tom", 20);
	printPerson(p);
}


//2、全局函数在类外实现
void test02()
{
	Person <string, int >p("Jerry", 30);
	printPerson2(p);
}

int main() {

	//test01();

	test02();

	system("pause");

	return 0;
}

  全局函数类外实现需要加一个空参数列表<>

9.2 STL 容器

  长久以来,软件界一直希望建立一种可重复利用的东西。

  C++的面向对象泛型编程思想,目的就是复用性的提升

  大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作。为了建立数据结构和算法的一套标准,诞生了STL

  STL(Standard Template Library,标准模板库)。

  STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)容器算法之间通过迭代器进行无缝连接。

  STL 几乎所有的代码都采用了模板类或者模板函数。

  STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

  1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
  2. 算法:各种常用的算法,如sort、find、copy、for_each等
  3. 迭代器:扮演了容器与算法之间的胶合剂。
  4. 仿函数:行为类似函数,可作为算法的某种策略。
  5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
  6. 空间配置器:负责空间的配置与管理。

9.2.1 容器、算法、迭代器

  容器:——置物之所

  STL容器就是将运用最广泛的一些数据结构实现出来。常用的数据结构:数组, 链表,树, 栈, 队列, 集合, 映射表等。

  这些容器分为序列式容器关联式容器两种:

  •   序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置。
  •   关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系。

  算法:——问题之解法

  有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)。

  算法分为:质变算法非质变算法

  • 质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等。

  • 非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等。

  迭代器:——容器和算法之间粘合剂

  提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。每个容器都有自己专属的迭代器。

  迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针。

  迭代器种类:

种类功能支持运算
输入迭代器对数据的只读访问只读,支持++、==、!=
输出迭代器对数据的只写访问只写,支持++
前向迭代器读写操作,并能向前推进迭代器读写,支持++、==、!=
双向迭代器读写操作,并能向前和向后操作读写,支持++、–,
随机访问迭代器读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器读写,支持++、–、[n]、-n、<、<=、>、>=

  常用的容器中迭代器种类为双向迭代器,和随机访问迭代器。

9.2.2 Vector容器

  STL中最常用的容器为Vector,可以理解为数组。

  容器:vector

  算法:for_each

  迭代器:vector<int>::iterator

  使用前要包含头文件 vector

#include <vector>
#include <algorithm>

void MyPrint(int val)
{
	cout << val << endl;
}

void test01() {

	//创建vector容器对象,并且通过模板参数指定容器中存放的数据的类型
	vector<int> v;
	//向容器中放数据
	v.push_back(10);
	v.push_back(20);
	v.push_back(30);
	v.push_back(40);

	//每一个容器都有自己的迭代器,迭代器是用来遍历容器中的元素
	//v.begin()返回迭代器,这个迭代器指向容器中第一个数据
	//v.end()返回迭代器,这个迭代器指向容器元素的最后一个元素的下一个位置
	//vector<int>::iterator 拿到vector<int>这种容器的迭代器类型

	vector<int>::iterator pBegin = v.begin();
	vector<int>::iterator pEnd = v.end();

	//第一种遍历方式:
	while (pBegin != pEnd) {
		cout << *pBegin << endl;
		pBegin++;
	}

	
	//第二种遍历方式:
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		cout << *it << endl;
	}
	cout << endl;

	//第三种遍历方式:
	//使用STL提供标准遍历算法  头文件 algorithm
	for_each(v.begin(), v.end(), MyPrint);
}

int main() {

	test01();

	system("pause");

	return 0;
}

  Vector存放自定义数据类型:

#include <vector>
#include <string>

//自定义数据类型
class Person {
public:
	Person(string name, int age) {
		mName = name;
		mAge = age;
	}
public:
	string mName;
	int mAge;
};
//存放对象
void test01() {

	vector<Person> v;

	//创建数据
	Person p1("aaa", 10);
	Person p2("bbb", 20);
	Person p3("ccc", 30);
	Person p4("ddd", 40);
	Person p5("eee", 50);

	v.push_back(p1);
	v.push_back(p2);
	v.push_back(p3);
	v.push_back(p4);
	v.push_back(p5);

	for (vector<Person>::iterator it = v.begin(); it != v.end(); it++) {
		cout << "Name:" << (*it).mName << " Age:" << (*it).mAge << endl;

	}
}


//放对象指针
void test02() {

	vector<Person*> v;

	//创建数据
	Person p1("aaa", 10);
	Person p2("bbb", 20);
	Person p3("ccc", 30);
	Person p4("ddd", 40);
	Person p5("eee", 50);

	v.push_back(&p1);
	v.push_back(&p2);
	v.push_back(&p3);
	v.push_back(&p4);
	v.push_back(&p5);

	for (vector<Person*>::iterator it = v.begin(); it != v.end(); it++) {
		Person * p = (*it);
		cout << "Name:" << p->mName << " Age:" << (*it)->mAge << endl;
	}
}


int main() {

	test01();
    
	test02();

	system("pause");

	return 0;
}

  Vector容器嵌套容器:

#include <vector>

//容器嵌套容器
void test01() {

	vector< vector<int> >  v;

	vector<int> v1;
	vector<int> v2;
	vector<int> v3;
	vector<int> v4;

	for (int i = 0; i < 4; i++) {
		v1.push_back(i + 1);
		v2.push_back(i + 2);
		v3.push_back(i + 3);
		v4.push_back(i + 4);
	}

	//将容器元素插入到vector v中
	v.push_back(v1);
	v.push_back(v2);
	v.push_back(v3);
	v.push_back(v4);


	for (vector<vector<int>>::iterator it = v.begin(); it != v.end(); it++) {

		for (vector<int>::iterator vit = (*it).begin(); vit != (*it).end(); vit++) {
			cout << *vit << " ";
		}
		cout << endl;
	}

}

int main() {

	test01();

	system("pause");

	return 0;
}

9.2.3 vector容器

  功能:vector数据结构和数组非常相似,也称为单端数组。不同之处在于数组是静态空间,而vector可以动态扩展

  **动态扩展:**并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间。

  vector构造函数:创建vector容器

函数原型:

  •   vector<T> v; 采用模板实现类实现,默认构造函数
  •   vector(v.begin(), v.end()); 将v[begin(), end())区间中的元素拷贝给本身。
  •   vector(n, elem);构造函数将n个elem拷贝给本身。
  •   vector(const vector &vec);拷贝构造函数。
#include <vector>

void printVector(vector<int>& v) {

	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		cout << *it << " ";
	}
	cout << endl;
}

void test01()
{
	vector<int> v1; //无参构造
	for (int i = 0; i < 10; i++)
	{
		v1.push_back(i);
	}
	printVector(v1);

	vector<int> v2(v1.begin(), v1.end());
	printVector(v2);

	vector<int> v3(10, 100);
	printVector(v3);
	
	vector<int> v4(v3);
	printVector(v4);
}

int main() {

	test01();

	system("pause");

	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值