C++的一点笔记

0、附

  1. vs2019快捷键:ctrl + k + f 代码自动对齐 ctrl + k + c 注释 ctrl + k + u取消注释
  2. c++关键词:cohesion(凝聚性) polymorphic 多态

1、关于对象

1.对象的定义,声明和初始化

  • 关于定义:定义必须以字母或者下划线开头,不能包括空白。变量声明的实质:将一段计算机内存和变量名关联起来。构造只能进行一次
  • 关于声明:变量声明是一个语句,指定变量名称和类型,常常声明和定义会同步进行,它只是说明变量定义在程序的其他地方,在其他地方已经完成了定义,这里只是说明有这么个变量存在。
  • 关于初始化:将初始值赋给变量的声明称为初始化,一般有三种方式:
1
2
3
int m1 = 10; //变量赋值
int m2 (10); //函数赋值(像构造函数)
int m3 {3}; //初始化列表(多个参数的)
  • 关于对象的位置:

    • 有且仅有new分配的对象在里。

    • 全局变量,静态局部变量和静态成员变量在全局数据区

    • 局部变量在堆栈

2.动态内存分配

  • 关键词:new 分配一段空间,返回值是该类型的一个指针(在堆区动态申请内存)
1
2
3
4
5
int *a = new int;
int *b = new int(10); //b指向10
int *c = new int[10]; //10个数的指针

int **p = new int*[10]; //这10个变量都是int型指针

new和malloc区别:new是操作符,malloc是函数,new会调用构造函数,malloc不会

  • 关键词:delete 会执行析构函数
1
2
delete p;
delete[] p; //p指向数组
  • 注意:
  1. 如果new时有[],delete也需要[]。
  2. 只有new分配的空间,才能执行delete。
  3. delete回收一段数组时,(参数是数组名),析构只会执行一次,只回收第一个了。

3.引用(Reference)

  • 引入:
  1. 引用就是给变量取了另外一个名字
  2. 变量在函数参数表只是声明,传入实参才是初始化。
  3. 引用绑定完不能去绑定别的
  4. 引用无法绑定引用
  5. 没有引用数组,也没有指向引用的指针
1
2
3
typename &refname = name; //引用必须在定义的同时初始化

void fun(int &i); //引用作为形参,调用时fun(X),X必须确定且独立。

4.const(对标宏)

1
2
3
4
5
6
const int x = 12; //x需要是一个常整数

string s = "abc";
char *const p = s; //p is const
const char *p = s; //(*p) is const
char const *p = s; //(*p) is const 也就是只有const 和 p 放在一起才是p为常量

注意:c语言声明数组长度必须已知。

  • 还可以放在函数声明的末尾,声明它不会改变参数的值
1
bool operator==( const Integer& rhs) const; 

2、输入输出

1.cout函数格式控制

  • 缩进(格式控制)
1
2
3
4
5
6
7
8
#include <iomanip> //头文件
cout.flags(ios::left); //左对齐
cout << setw(10) << -456.98 << "The End" << endl; //setw(10)默认填充空格

cout << left << setw(10) << -456.98 << "The End" << endl; //左对齐
cout << internal << setw(10) << -456.98 << "The End" << endl; //两端对齐
cout << right << setw(10) << -456.98 << "The End" << endl; //右对齐

  • 小数点位数控制
1
2
3
4
#include <iomanip> //头文件
cout.setf(ios::fixed);
cout << fixed << setprecision(N) <<X<< endl; //控制X小数点后输出两位
cout.unsetf(ios::fixed); //取消补0,之后不会再补0

2.输入:getline函数(对比cin)

  • 格式:

    1
    2
    getline(cin,str,'结束字符');
    cin.getlinr(ch,n(读取数量),'结束字符');
  • 结束字符默认为回车

  • 上一个cin残留的回车会在这里再次被吃掉!

  • cin从数据流里读取数据后,会有一个指针实时指向应该访问的流,即结尾的空格和回车会影响下一次的读入。

  • 解决方案:scanf和cin会把结束字符留在缓冲区里,cin.getline()和gets会把缓冲区里的垃圾字符移走!

3、class

1.类的定义

  • 会将类的定义放在独自的头文件里,同时将类中函数的定义和实现分开,类的定义中只留函数首部。类会分成a.h和a.cpp

    两个文件,在函数实现时需写成:

    1
    void A::print(int a) //其中A为类名
  • 好处:main.cpp代码和编译不会受class里变更影响,缩短编译时间。

2.:: resolver

  • 限定访问范围,在限定范围的函数中,如果不加::,则是默认调用限定范围内,加上::则且无前缀则可以认为是全局访问的函数。
1
2
void A::f1(){}
void ::f2(){} //可以全局访问

3.header files

  • header=interface 提供user和class的作者间的接口。
  • 在预编译时会将头文件里的代码与main文件链接在一起。
  • #include: “ ” 和<>的区别:<>在系统预设的环境中寻找,””完全与实现相关,一般会先去当前目录下寻找。

4. 类中嵌套类的使用

  • 由于每个头文件中都会引用其他文件,会导致重定义。

    解决办法:头文件保护

1
2
3
4
#ifndef A_H
#define A_H
...
#endif

5. 构造函数

  • 因为class的变量一般是private,所以通过一个构造函数去使用class中的变量。

  • 析构函数不能带参数,构造函数是能带参数的。

  • 构造函数不带返回值,当对象被创建时自动被调用。

  • 难点:默认构造函数,不适用初始化列表,直接在构造函数内部构造,则被构造的变量需要有一个默认的构造函数

  • 如果成员变量里有const修饰的,必须要用初始化列表进行初始化,不能用赋值

  • 默认构造函数:如果我们没有写构造函数,系统会自动帮我们补上一个默认构造函数,如果我们写了构造函数,默认构造函数将被取缔,但我们可以手动添加一个默认构造,

    添加方式有:

    • 添加一个什么都不做的:A2();
    • 添加一个所有参数都有默认值的构造:A2(int a =10);

    一个类只会有一个构造函数!

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
49
50
51
52
53
54
55
56
class A
{
private:
int x;
public:
//其中3是成员变量的默认值,必须要从右往左赋值
A(int y = 3){
x = y;
}
int A::add(int m){
}
//const 说明我这个成员函数不会修改成员变量的值
void A::print() const{

}
}

class A2{
private:
int a2;

public:
A2(int m):A2(m){};
A2(); //我们手动添加的默认构造函数,系统生成的也是这个
A2(){a2 = 10;}//和上一句只能二选一
}

class B
{
private:
int m;
int n;
}

class C
{
private:
int a;
B b;
public:
C(int x1,int m,int n = 5):a(x1),b(m,n){} //初始化列表先与花括号内,保证非内置的成员在初始化列表上得到执行,放在花括号内是赋值语句,不同于初始化列表。
}
//使用初始化列表就是少了一次调用默认构造函数的过程!使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。常量成员,初始化没有默认构造函数的对象是必须使用初始化列表的。

//**且常量必须要用初始化列表不能用赋值,因为常量只能初始化**
void foo() const {}
}
//函数后加上const关键词,承诺该函数是不会改变数据的。

int main()
{
A t(10);
A a1[3] = {A(1), A(2), A(3)}; //没有问题!
A a2[3] = {A(1), A(2)}; //编译器会报错!编译器会补上一个A(),但找不到参数。
B b1[3]; //compiler(编译器)自动补上B(){}。
}

6. this关键词

  • 定义:成员函数的隐藏变量
1
2
3
4
5
void Point::print(Point *this); //this 被隐藏

Point a;
a.print();
Point::print(&a);

7.inline function (内联函数)

  • 定义:简单来说就是编译时直接展开代码,减少开销,所以会增大可执行文件体积,所以适合一些较小的函数,两到三行就能解决的。
  • 如果声明和定义在不同文件,(声明在头文件),会报错!必须在完整定义放在头文件中。
  • 实现:在函数定义前加上inline关键词:(函数声明前可以不加)
1
2
3
inline void f(int x){ 
cout << x << endl;
}

8.友元函数

  • 友元函数不是一个类的成员函数,但是它可以访问类的private变量,它没有this指针。
  • 必须要在函数内部声明,且一个友元函数可以是多个类的朋友,在多个类中分别声明。
  • 一个类的成员函数可以成为其他类的友元函数,在声明为另一个类的friend,需要将原类的作用域加上。
  • 可以将一个类作为另一个类的友元类,原类中的所有函数都是另一个类的友元函数,
  • 友元关系不被继承,不具有传递性,且是单向的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void print(Point &a);

class SpecialPoint
{

};

class ManagePoint
{
public:
double distance(Point &a, Point &b);
};

class Point
{
friend SpecialPoint; //友元类
public:
Point(double x,double y):m_x(x),m_y(y){}
friend void print(Point &a); //全局函数做友元声明
friend double ManagePoint::distance(Point &a, Point &b); //成员函数做友元声明
private:
double m_x;
double m_y;
};

9.static变量

  • 静态成员变量属于整个类所有,所有对象共享类的静态成员变量

  • 静态成员变量生命周期不依赖于任何变量,为程序的生命周期。

  • 可以通过类名直接访问共有静态成员变量,可以通过对象名访问共有静态成员变量。

  • 静态成员变量在类外单独分配空间。

4、composition & Inheritance (组合与继承)

1.composition

  • composition: construct new object with existing objects.

  • 大class的某一个函数可以是组成他的数个小class,执行每一个class的函数。

  • 组合里的成员一般不做成public。

1
2
3
4
5
6
7
class SavingsAccount{
SavingsAccount::SavingsAccount(const char* name, const char* address, int cents): m_saver(name, address), m_balance(0, cents) {}
void SavingsAccount::print(){
m_saver.print();
m_balance.print();
}
}
  • 所有的迁入对象都会初始化,如果没有提供足够的参数,会调用默认的构造函数

2.inheritance

  • base->derived ,super->sub (base,super基础,derived sub为派生类)

  • 派生拥有基础类的所有性质,但有基础类没有的特性。

  • 派生类只能访问protected,只有friend可以访问private。

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
class A
{
public:
A(int x1, int y1):x(x1),y(y1){}
void print(){}

A()
protected: //专门为继承服务
int m;
int x,y;

};

class C : public A //继承A
{
public:
C(int i1,int i2, int i3):A(i1,i2),z(i3){}
//如果基类没有默认构造,继承类必须初始化基类和继承类的全部变量。基类有默认构造情况,且没有明确参数传给基类,基类就会调用默认构造
void set(){m = 1}; //protected的对象可以在派生类中使用
void print(){} //与A的print重复,这里会优先调用派生类的print

private:
int z;

}

int main()
{
C m;
m.x = 1; //可以直接调用A中变量
m.Base:print(); //两个print重名

}
  • 先构造基类 再构造继承,先析构继承,再析构基类!

  • 派生类可以调用基类的函数,也可以重载它。如果基类和派生类有同名的函数,前参数一致,派生类会先调用自己的函数

5、多态

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
class shape
{
private:
public:
void move()
{}
virtual void render(){} //虚函数 虚函数中有虚函数表vptr,里面存放这一类虚函数的地址
};

class ellipse:public shape
{
public:
void render(){}
};

class circle:public ellipse
{
void render(){}
};

void foo(shape *s) //此时main里该函数传入circle,ellipse 的指针,就会调用ellipse,circle的render函数
{
s->render();
}

2.Polymorphism(多态性)

  • Upcast: take an object of the derived class as an object of the base one.
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
class Shape { 
public:
Shape();
virtual ~Shape();
virtual void render();
void move(const Point&);
virtual void resize();
protected:
Point center;
};

class Ellipse: public Shape{
public:
Ellipse(float major,float minor);
virtual void render();
protected:
float major_axis,;
float minor_axis;
};

class Circle: public Ellipse{
public:
Circle(float radius);
virtual void render();
virtual void resize();
virtual float radius();
protected:
float area;
}

image-20220621170415358

  • virtual关键词:

    核心是为了让程序只调用一个函数。可以实现多种功能,在C++程序类的继承中,实现函数的重写(Override)

    加入virtual前缀的是虚函数,基类有virtual修饰,派生类的同名函数也会被virtual关键词修饰

  • 在以下的代码中,如果分别定义一个person类的变量p,student类的变量s,没有virtual关键词,那么调用print函数会分别调用,但如果我生成两个person类的指针分别指向p,s,再调用这两个指针,则都会调personA中的函数,加上virtual,则会分别调用两个函数。

    有意思的事:那个指向s的指针,他会存下变量,但不能调用student中有person没有的函数?

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
class person{
public:
person(string n):name(n){
std::cout << "person: ()" << std::endl;
}
virtual void print(){
cout << name << endl;
}
protected:
string name;
};

class student:public person{
public:
student(int i, string n):id(i),person(n){
cout << "student:()" << endl;
}

void print(){
cout << name << endl;
cout << id << endl;
}

void printid(){
cout << id << endl;
}

private:
int id;
};

再看以下测试代码:(椭圆,圆的例子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ellipse elly(20F, 40F);
Circle circ(60F);
elly = circ; //赋值给elly时,circ的area变量会没有。

Ellipse *elly = new Ellipse (20F, 40F);
Circle *circ = new Circle(60F);
elly = circ;
//第一段ellipse的空间会小时,elly和circ共同指向circ。

void func(Ellipse& elly) {
elly.render();
}
Circle circ(60F);
func(circ);
//引用的行为是类似于指针的,所以这里func内部调用的是circle的render。

注意:

  • 如果基类的析构函数要被继承,把它设置为virtual,否则派生类在析构时只会析构基类的部分,不会析构额外的部分。
  • 如果基类的虚函数后加const,派生类相应的函数没有加const,那么虚函数没有起到相应的作用,如果派生类相应的函数也加const,虚函数机制又发挥相应的作用了

3.override关键词

  • 必须存在于继承关系中,重写只能出现在子类中,且函数声明必须和基函数一直(返回值,参数列表都要一致)

    如果是overloading(重载),参数必须不一致

  • 目的:1.在函数比较多的情况下可以提示读者某个函数重写了基类虚函数(表示这个虚函数是从基类继承,不是派生类自己定义的);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
class person{
public:
person(string n):name(n){
std::cout << "person: ()" << std::endl;
}
virtual void print(){
cout << "This is a person " << name << endl;
}
protected:
string name;
};

class student:public person{
public:
student(int i, string n):id(i),person(n){
cout << "student:()" << endl;
}
//这个函数我重写了基类的print
void print() override
{
person::print();
cout << "This is his id: " << id << endl;
//会输出名字和学号
}

void printid(){
cout << id << endl;
}

private:
int id;
};

4.抽象类

  • 含有纯虚函数的是抽象类:定义为纯虚函数后,该类不能生成对象,只能派生
  • 派生类要是没有实现抽象类,则派生类还是抽象类
  • 纯虚函数的声明:
1
2
3
4
5
6
7
8
9
class CDevice {
public:
virtual ~CDevice() {}
virtual int read(...) = 0;
virtual int write(...) = 0;
virtual int open(...) = 0;
virtual int close(...) = 0;
virtual int ioctl(...) = 0;
};

6、拷贝构造

1.类内部的引用

  • 如果类的变量有引用,必须要用一个初始化列表来给引用绑定初值(不能后面用赋值)
  • 如果类的参数中有引用,且声明为const,那么这个变量在函数内部不能被修改,但是他所绑定的对象在函数外部还是可以被修改的,相当于声明这个函数不会修改这个值。
1
2
3
4
5
void func(const int& y, int& z) {
z = z * 5; // ok
y += 8; // error!
};
//而且y的值不能传入一个类似于i*3的结构,会警告或者报错
  • 引用作为返回值,引用作为函数返回值实际是一个指向返回值的隐式指针,如果类返回一个引用,这个引用不能绑定局部变量(会被销毁!)
1
2
double &operator[](int i); //返回对象本身,可以对其进行操作,不生成引用的副本。
double operator[](int i)const; //只返回一个数值。
  • 把一个未声明对象的构造函数作为形参传入,需要用new分配动态空间。
1
LinkList ll(*new StrNode(word));

2.拷贝构造

  • 使用一个const 引用作为构造函数的形参,会复制所有值。如果类中有指针,指针指向的block也会被复制一份

    为什么拷贝构造函数的形参要用引用?因为如果不用引用,实参到形参的传递是传值的方式,传值是会调用拷贝构造的,所以会无穷递归地调用拷贝构造。

  • 调用拷贝构造有以下几种形式:

1
2
3
4
5
Person baby_a("Fred"); //普通构造

Person baby_b = baby_a; // 用一个类去初始化另一个类,不是赋值,也是拷贝构造
Person baby_c( baby_a ); //调用拷贝构造函数
fun(baby_a);//fun为一个以Person类作为形参的函数(不一定要是引用!),用实参拷贝构造形参,是拷贝构造
  • 默认拷贝构造下,编译器将自动给所有成员对象拷贝。

3.浅拷贝与深拷贝

  • 浅拷贝:

    由系统提供的拷贝,可以理解为用等号一个个赋值。(指针也会直接赋值)

    那么如果原指针指向的内容被释放,拷贝后的指针无法正常释放。

    当类中无拷贝构造时,系统会设置一个默认的拷贝构造,是浅拷贝

    会出现doublefree的问题。

  • 深拷贝

    如果类中有动态内存申请,必须重写拷贝构造,让新的指针指向新的空间、

7、重载

1.conversion operation

1
2
3
4
operator double() const{
return numerator/(double)denomirator;

}

2.函数重载:

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}

void print(double f) {
cout << "浮点数为: " << f << endl;
}

void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};

3.运算符重载

  • 函数名:类名 operator”符号”,形式参数里的形式:const 类名&。
  • 可以用作成员函数,也可以用作全局函数,全局的重载函数还可以成为一个友元函数
  • 关于返回值:
    • 如果是<,>,==,返回bool类型的返回值。
    • 如果是+,-,返回一个新的对象
1
2
3
4
5
6
7
8
9
10
11
Box operator+(const Box&, const Box&);

//成员函数中第一个参数设为隐式(this)
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
  • Q:为什么它作为成员函数可以访问其他实例的私有变量?

    A:private控制的是类型级别的访问权限,实例T内部的成员函数可以访问同一类型的T的其他实例的私有成员。

  • ++运算符重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Integer& Integer::operator++() { 
this->i += 1; // increment
return *this; // fetch
}
//++i,无需参数,返回改变后的值
// int argument not used so leave unnamed so
// won't get compiler warnings

//i++,返回改变前的值,这个int参数只是一个占位符,
Integer Integer::operator++( int ){
Integer old( *this ); // fetch
++(*this); // increment
return old; // return
}
  • 重载[]: 必须是一个成员函数,只有一个参数,
  • 重载=:必须是一个成员函数,返回值是一个引用,引用绑定(*this)
1
2
3
4
5
6
7
8
9
T& T::operator=(const T& rhs)
{
//check for self assignment
if ( this!= &rhs)
{
//perform assignment
}
return *this;
}

4.隐式转换

  • 定义:将实参类型转换为形参类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//apple 是 orange 的成员变量类型
class orange
{
public:
orange(apple&){}
};
void f(orange o){}

int main()
{
apple a;
f(a);
}

  • explicit 关键词可以防止隐式转换,加在函数前
1
2
3
4
5
6
7
8
9
10
class PathName{ 
string name;
public:
explicit PathName(const string&);
~ PathName();
};
...
string abc("abc");
PathNamexyz(abc); // OK!
xyz = abc; // error!

5.functor模仿函数与匿名函数(课件里找不到了,先放着吧,用到了再来仔细解释)

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
struct F{
void operator()(int x) const
{
}
}
F f; //不是真的函数,是模仿的函数。
f(2);

//匿名函数
void transform(vector<int>&v, int (*f)(int)) //第二个是函数指针
{
}

//有a需要修改为:
void transform(vector<int>&v, function<int(int)> f) //第二个是函数指针
{
}

class mul_by{
public:
mul_by(int a):a(a){}
int operator()(int x) //定义一个仿函数
{
return a*x;
}
private:
int x;
}

int main()
{
transform(v,[](int x){return x*5;}); //在传入时定义函数运算
int a = 7;
transform(v,[a](int x){return x*7}); //在[]中输入参数。
transform(v,mul_by(5));
}


6.类型转换函数

  • 有以下要求:

    转换函数必须是类的成员函数

    转换函数不能声明返回类型
    形参列表必须为空
    类型转换函数通常应该是const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Str{
char *m_p;
char m_s[10];

public:
Str(char *s){
strcpy(m_s, s);
m_p = m_s;
}

//函数1
char operator*(){
return *m_p;
}

char *operator++(){
return ++m_p;
}

//并不能说是重载,它把结果转换为char* 类型。所以作用相当于函数1
operator char*(){return m_p;}
};

8、stream 流

1.流的引入与优缺点

  1. 什么是流?

    • Common logical interface to a device
  2. advantage

    • better type safety

    • extensible (可扩展)

    • more object - oriented

  3. disadvantage

    • more verbose(冗长)
    • might be slower (流与printf有同步机制,降低速度,可以关闭同步)

2.header

1
2
3
#include<iostream>
#include<ftream //文件流
#include<sstream> //字符串流

3.流的信息

  • 流可分为文本流和二进制流,文本流处理ASCll text
  • << 向流中写东西, >> 从流中提取东西。

4.几种流(各自都有缓冲区)

image-20220621201135877

  • 关于iostream

    ceer:未缓冲的错误(调试)输出,

    clog: 缓冲的错误(调试)输出

  • 定义一个流的重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
istream& operator>>(istream& is, T& obj) {
// specific code to read obj
return is;
}

class Person
{
public:
friend ostream& operator<<(ostream& os, const Person& p); //声明operator<<为友元函数
Person(string name, int age):name(name), age(age) {}
private:
string name;
int age;
};
ostream& operator<<(ostream& os, const Person& p) //返回值为ostream类用
{
cout << "name:" << p.name << " age:" << p.age << endl;
return os;
}

//cin >> (>> 称为extractor)
//有返回值,相当于(cin>> a)>> b ;顺序为从左往右。
  • 其他输入操作符
1
2
3
4
5
get(char *buf, int limit, char delim='\n');
//limit为字符数限制,\n为结束符。

getline(char *buf, int limit, char delim='\n');

  • cout . flush()

    强制输出流的内容。

1
2
cout<< "Enter a number";
cout.flush();

9、template

1.引入

  1. 前提:避免重复代码(不要重复自己)
  2. 使用模板:generic programming 的一种(泛型编程)。核心:在类与函数的定义中使用一些不同的类型参数。成立根源:cpp可以实现函数重载

2.函数模板

  1. function template(函数模板)一般完整地放在头文件中
    只有在调用时才会产生真正的实例函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//普通函数和下面的函数模板共存时,普通函数优先。
void my_swap( int &x, int &y )
{
int tmp = x;
x = y;
y = tmp;
}

// T为类型参数
template <class T> //这里class也可以写成typename
void my_swap( T &x, T &y )
{
T tmp = x;
x = y;
y = tmp;
}

int main()
{
float x = 0.5, y = 0,4;
my_swap(x,y); //可以正常执行,在这里将float传给T,生成实例函数。
my_swap<double>(x,y); //如果x与y类型不同,可以确定一个类型参数。
}

T可以出现在参数列表,函数主体与返回值中。

不允许隐式转换!(应该是我调用函数时不能隐式转换把):question:

  • 选择模板执行顺序:参数完全匹配->模板类->需要强制类型转换,模板类中,顺序为参数类型一致(有无 const),然后全特化->偏特化->参数需要强制类型转换。

3.class template(类模板):

  • 类模板中的成员函数自动成为函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这个Vector是自己定义的
template <class T, int bounds = 100> //这个bounds就是一个普通参数,调用这个class可以传入具体值,如果不传入bounds,就会调用默认值
class Vector{
private:
T element;
int elements[bounds];
}
//构造函数
template <class T>
Vector<T>::Vector(int size):element(size){}

int main()
{
Vector<int> v1(100);

}
  • 类模板可以和继承结合。类模板可以继承类模板,类模板可以继承模板类,类模板可以继承普通类,普通类可以继承模板类
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
using namespace std;

//1、类模板继承类模板
template <typename T1, typename T2>
class A
{
T1 x;
T2 y;
};

template <typename T1, typename T2>
class B : public A<T2, T1>
{
T1 x1;
T2 y2;
};

template <typename T>
class C : public B<T, T>
{
T x3;
};

//2、类模板继承模板类
template <typename T>
class D : public A<int, double> //具体化的模板类
{
T x4;
};

//3、类模板继承普通类
class E
{
int x4;
};
template <typename T>
class F : public E
{
T X5;
};

//4、普通类继承模板类
template <typename T>
class G
{
G g;
};
class H : public F<int>
{
int h;
};

int main()
{
//1、类模板继承类模板
C<int> c; //由派生的具体类型反推 基类 模板类型 C<int> B<int, int> A<int, int>
B<int, char *> b; //由派生的具体类型反推 基类 模板类型 B<int, char*>, A<char*, int>

//2、类模板继承模板类
D<float> d; //生成D<float> 和 A<int, double> 模板类

//3、类模板继承普通类
F<bool> f; //生成 F<bool>

//4、普通类继承模板类
H g; //生成 F<int> 模板类
return 0;
}
  • 注意:函数模板可以重载,类模板不能重载。(没有引入一个全新的模板或者模板实例,对原来的泛型模板的实例提供另一种定义)

4.模板全特化

  • (指定的模板实例必须和相应的模板参数列表一一对应)
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
template<typename T1, typename T2>
class A{
public:
void function(T1 value1, T2 value2){
cout<<"value1 = "<<value1<<endl;
cout<<"value2 = "<<value2<<endl;
}
};

template<>
class A<int, double>{ // 类型明确化,为全特化类
public:
void function(int value1, double value2){
cout<<"intValue = "<<value1<<endl;
cout<<"doubleValue = "<<value2<<endl;
}
};


template<typename T>
class A<T, double>{ // 部分类型明确化,为偏特化类
public:
void function(T value1, double value2){
cout<<"Value = "<<value1<<endl;
cout<<"doubleValue = "<<value2<<endl;
}
};

int main(){
A<int, double> a;
a.function(12, 12.3);
return 0;
}

5.模板偏特化

  • 偏特化:将部分参数特化为一确定值,将模板参数特化为指针或者其他模板类
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
template<typename T, class N> void compare(T num1, N num2) {
cout << "standard function template" << endl;
if(num1>num2) {
cout << "num1:" << num1 << " > num2:" << num2 <<endl;
} else {
cout << "num1:" << num1 << " <= num2:" << num2 << endl;
}
}

// 对部分模板参数进行特化
template<class N> void compare(int num1, N num2) {
cout<< "partitial specialization" <<endl;
if (num1>num2)
cout << "num1:" << num1 << " > num2:" << num2 << endl;
else
cout << "num1:" << num1 << " <= num2:" << num2 << endl;
}

// 将模板参数特化为指针(模板参数的部分特性)
template<typename T, class N> void compare(T* num1, N* num2) {
cout << "new partitial specialization" << endl;
if (*num1>*num2)
cout << "num1:" << *num1 << " > num2:" << *num2 << endl;
else
cout << "num1:" << *num1 << " <= num2:" << *num2 << endl;
}

// 将模板参数特化为另一个模板类
template<typename T, class N> void compare(std::vector<T>& vecLeft, std::vector<T>& vecRight) {
cout << "to vector partitial specialization" << endl;
if (vecLeft.size()>vecRight.size())
cout << "vecLeft.size()" << vecLeft.size() << " > vecRight.size():" << vecRight.size() << endl;
else
cout << "vecLeft.size()" << vecLeft.size() << " <= vecRight.size():" << vecRight.size() << endl;
}

10、STL

1.引入

  1. 全称:标准模板库 standard template library 源代码建议自行学习。

  2. 包括:pair, list, vector, deque, set, 都在std的命名空间下。分为容器,算法。迭代器三大类。

2.container:

vector(动态数组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector>  //头文件
vector<T> v1; //T为类名,默认v1为空
vector<T> v2(v1); //v2是v1的一个副本
vector<T> v3(n,i);//v3包含n个值为i的元素
vector<T> v4(n); //v4含有值初始化的元素的n个副本;

vector<int> vec_sample(10); //创建10个元素,每个元素初始化为0.
vector<string> vec_sample(10); //创建10个元素,每个元素初始化为””(空字符串).

//以下为操作:
vector<int>::itrator p; //向量迭代器

vec.push_back(i); //新增元素i放在vector的末尾
vec.pop_back(); //删除末端元素
vec.erase(it);//删除指定位置的元素,其中it为指针
vec.begin(); //返回第一个元素的指针
vec.end(); //返回最尾端元素的下一个元素

//遍历
for(int &x : vec)
{
x++; //可以直接用x表示被遍历到的元素。
}
  • vector的capacity不是固定的,会随着压入元素而增长,且一般是成倍增长。每增长一次,会把之前的元素拷贝过来。
1
2
vector.reserve();//可以固定容器的capacity(?)
vector.emplace_back(); //构造时直接在vector上构造,直接传入vector的构造函数参数表,无拷贝构造操作
list(双向链表)
  • 迭代时和vector基本一致,迭代器的结束条件是it != list.end(); 具有以下方法。
1
2
3
4
5
6
list<int> l;
l.front(); //头部指针
l.back(); //尾部指针
l.push_back(item),
l.push_front(item) l.pop_back(),
l.pop_front() l.remove(item)
deque(两头可变的数组)

这里用到再补充把

forward

这里用到再补充把

map(关联式容器)
  • 通过键值查询东西,cpp底层map是一个红黑树,增删改查较为高效。

  • map key和value的类型可以自定义,自动建立Key-Value的对应,使用key值快速查找记录,查找的复杂度是O(logN)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <map>
map<string, float> price; //key为string, value 为 float

map <int,string> mapstudent;

mapstudent.insert(pair<int, string>(1,"student_one")); //插入一个学生信息

mapstudent.insert(map<int, string>::value_type (2, "student_two"));

mapstudent[3] = "student_three"; //插入元素从下标1开始

for(auto iter = mapstudent.begin(); iter != mapstudent.end(); iter++)
{
//打印学生信息与学号
cout << iter->first << " " << iter->second << endl;
}

for(int index = 1; index <= mapstudent.size(); index++)
{
cout << mapstudent[index] << endl;
}

  • map可以设置pitfall,用来查找某个值
1
2
3
4
5
if(mapstudent["bob"] ==1){}

if(mapstudent.count("bob")){}

if(mapstudent.contains("bob")){}

3.algorithms

  • (和容器没有直接关系,一般的参数是迭代器)

  • 一个例子:copy

    1
    2
    3
    4
    #include <algorithm>
    copy(L.begin(), L.end(), v.begin()); //将list L中的元素拷贝入vector v中。
    copy(L.begin(), L.end(), ostream_iterator<int>(cout,", ")); //拷贝到输出流迭代器中。
    copy(L.begin(), L.end(), infix_ostream_iterator<int>(cout,", ")); //考虑最后一个元素后没有空格这种问题。

4.iterator

  • 充当容器和算法之间的胶水
  • 看一个find算法
1
2
3
4
5
6
7
8
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last,const T &value)
{
while (first!=last && *first!=value)
++first;
return first;
}
//如果返回的是end那就说明没找到

image-20220621222549293

11、Exceptions

1.read a file:

顺序:

open the file , determine its size, allocate that much memory, read the file into memory, close the file

2.引入异常

  • assert:一般代表代码错误,但是异常可能是用户的问题

  • keyword:throw

    可以扔对象,原始类型,

  • 任何的try语句之后至少要有一个catch(), catch()很像一个函数

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
//throw 会导致函数终止,先离开局部函数,如果这个局部函数嵌套在另一个函数中,也会离开那个大的函数。
T &Vector<T>::operator[](int index){
if (idx < 0 || idx >= m_size)
{
throw VectorIndexError(idx); //问题类型:下标错误,问题信息:idx错误,这条语句后面的语句不会被执行,异常对象应该是自己写
}
return m_elements[idx];
}

void func(int i){
Vector<int> v1;
v1.emplace_back(2);
int i = V1[3]; //[]抛出异常会从这里结束函数,之后的语句不会被执行。
}


void outer() {
try {
func();
func2();
} catch (VectorIndexError& e) { //捕捉error,func()退出后,会进入catch。进入catch不会回去执行func2()。会进入catch后面的语句。
e.diagnostic();
// This exception does not propagate
}
cout << "Control is here after exception";
}

void outer2(){
String err("exceptioncaught");
try{
func();
}catch(VectorIndexError){
cout << err;
throw;// propagate the exception,扔出的是刚刚捉到的异常。因为catch已有参数
}
}

void outer3(){
try{
outer2();
}catch(...){ //就是写成...,是一个万能捕捉器
cout << "catch the error" ;
}
}
  • 有多个catch。如何匹配?针对每一个catch,运用这三个方案轮流查

    • exact match

    • base class conversion(子类的异常对象能不能被父类捉到),如果前一个是父类的error,后面是子类的error,那么子类的错误永远不会被catch到,所以过不了编译。

    • … match all

  • 在函数里限定抛出类型(我这个函数只有可能抛出这些异常)

1
2
3
4
5
6
7
8
9
10
11
12
void abc(int a):throw(MathErr){

}

//这个函数啥异常都不会抛出来
void print(int a):throw(){

}

//这个函数什么类型的异常都有可能抛出来
void find(int a){
}

3.异常举例

  • 如果new发现可供分配的空间不足,C++会抛出一个bad_alloc( )异常。

  • 构造函数中出现问题,可以抛异常,则对应的析构不会被调用,且如果用new会导致空间被分配,但是没用,总之风险很大。

image-20220622205446599

image-20220622205616172

12、类型转换与命名空间

1.类型转换

类型转换

类型转换分为四种

  • static_cast 静态转换

    支持任何隐式转换类型,但不支持两个不相关的类型进行强制转换(less likely to make mistakes)

  • dynamic_cast 将一个父类对象的指针转换为子类对象指针或者引用

    向上转型(子类->父类)不需要转换

    向下转型 (父类->子类),用dynamic_cast是安全的

    注意:只能用于含有虚函数的类,且它是先分配子类的空间,把他向上转为父类的指针,再往下转换为子类的。

  • reinterpret_cast 将一种类型转换为另一种不同类型,没有二进制的转换,一般用于指针

  • const_cast,去掉变量的const属性,方便赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = static_cast<int>(d);
//底层的字节数会发生改变,double转换为int
int i = 10;
char c = static_cast<char>(i);
//不能在两个具体类型的指针间进行转换,不能,将整数转换为指针类型

int a = 7;
double *p;
p = reinterpret_cast<double *>(&a);
//将四字节的int转换为8字节



const int c = 7; //常量所以无法把c的指针传给q
int *q;
q = const_cast<int *>(&c);


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
namespace old1 {  
void f();
void g();
class Cat {
public:
void Meow();
};
}
//一般把函数声明封装为命名空间,封装的命名空间放在头文件里。

//在函数定义时加上命名空间限制
void old1::f(){cout << " " << endl;}

//可以在主函数中限定函数和类的命名空间范围。也可以不说明具体函数,直接使所有来自该空间的函数和类可用,就像using namespace std;但这样子可能会造成歧义,那么就单独说明造成歧义的函数来自哪个空间

int main() {
using namespace std;
using old::f;
using old::cat;
foo();
Cat c;
}

//还可以给命名空间一个别名。
namespace supercalifragilistic
{
void f();
}
namespace short_ns = supercalifragilistic;
short_ns::f();
  • 一个命名空间可以被拆分分布在不同头文件中。

13、CMake

  • CMakelists.txt:(好处:可以跨平台使用)生成帮助编译的东西。
1
2
3
cmake minimum_required(VERSION 2.8.9) //版本信息
project(point_design) //项目名称
add_executable(point_design main.cpp point.cpp) //加入所有源文件

14、文件读写

1、文件写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<fstream>
ofstream o("map.txt",ios::out); //o为文件变量名,
//ios::out 文件只写
//ios::in 文件只读
//ios::ate 初始位置在文件尾
//ios::app 修改在文件尾
//ios::trunc 文件已存在则删除


if(o.is_open()) //判断文件是否正常打开
{
o<<"hello word!"<<endl;
o.close(); //关闭文件
}

15、随机函数

  1. 基本随机函数
1
2
3
4
5
6
7
8
9
10
#include<cstdlib>
#include<ctime>

srand(int(time(0)));
rand()%x; //输出[0,x)的随机整数
rand()%(b-a)+a; //输出[a,b)的随机整数
rand()%(b-a+1)+a; //输出[a,b]的随机整数
rand()%(b-a)+a+1; //输出(a,b]的随机整数
rand()/double(RAND_MAX);//输出0-1之间的浮点数

  1. 一个循环生成随机数的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#include<ctime>
using namespace std;

int main() {
srand((unsigned)time(NULL));
for (int i = 0; i < 10; i++) {
int j = rand() % 10;
cout << j << endl;
}
system("pause");
return 0;
}

16、string类的延伸使用

1
2
3
4
str.append("abc"); //字符串末尾加上一个新的字符子串
str.push_back('a');//字符串末尾加上一个新的字符。

itoa(100,string ,10); //第一个100为整数,string为字符串指针,10位进制。
  • 还有c中的一些函数
1
2
3
#include<cstring>
strlen(s);
strcpy(p,s); //把s赋值给p

17、智能指针

1.常见的智能指针

  • auto_ptr(c++11已经将其抛弃)

  • unique_ptr(两个unique_ptr)不能指向同一个对象,指针间无法值传递

    头文件:memory

    本身是一个类模板

  • shared_ptr,多个指针可以指向同一个对象,

    头文件:memory

  • weak_ptr,与shared_ptr共同工作,不具有普通指针的行为,

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
//unique_ptr
// 智能指针的创建
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //绑定动态对象
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d); //创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

// 所有权的变化
int *p_i = u_i2.release(); //释放所有权
unique_ptr<string> u_s(new string("abc"));
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针”
u_s2.reset(u_s.release()); //所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价


// weak_ptr
int main() {
shared_ptr<int> sp(new int(10));
assert(sp.use_count() == 1);
weak_ptr<int> wp(sp); // 从 shared_ptr 创建 weak_ptr
assert(wp.use_count() == 1);
if (!wp.expired()) { // 判断 weak_ptr 观察的对象是否失效
shared_ptr<int> sp2 = wp.lock(); // 获得一个 shared_ptr
*sp2 = 100;
assert(wp.use_count() == 2);
}
assert(wp.use_count() == 1);
cout << "int:" << *sp << endl;
return 0;
}

18、杂七杂八

1.对象交互

  • 首要原则:封装

  • 设计时考虑:抽象,拆分

  • 当类里的函数需要有先后顺序(比如需要一个初始化函数)

    • 一些构造函数需要强制执行。
    • 内存开辟(定义变量)后未初始化,vs会自动填充‘0xcccc’

2.enum(枚举)

  • enum:一种基本数据类型,它可以让数据更简洁,更易读。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum day{
mon = 1,
twe,
wed,
};
//twe开始自动为上一个加1。

enum Day{
sun, //默认为0
twe, //1
wed = 4,
thu, //5

}

int main(){
day d1 = sun;
}

3.关于指针与数组头大小计算

  • 可以把一个一维数组的头赋值给一个一级指针,但是指针的大小是指针本身,数组的头的大小是数组的大小。

  • 不可以把一个二维数组的头赋值给一个二级指针,二级指针的大小和一级指针大小一致,数组的头的大小是整个二维数组大小。

    主要原因可能是两者大小冲突,而一维数组的由于长度不缺定可以退化到指针。

    *但如果我把二级指针写成int (z)[3]的形式,即一个数组指针,这时就可以直接赋值。这时z和*z的值相同,而且z的大小还是一个指针变量的大小。

  • 指针变量大小为8个字节。


C++的一点笔记
http://example.com/2022/10/22/C++的一点笔记/
作者
Mr Pony
发布于
2022年10月22日
许可协议