第七章 函数-C++的编程模块

函数的基本概念和应用:

1
2
3
4
5
具体应用
按值传递,按地址传递
函数在数组中的应用
函数在结构体中的应用
函数在string中的应用

1、使用C++函数三部曲

提供函数定义:根据返回值来定义函数,在函数中用return返回(函数通过将返回值复制到指定的CPU寄存器或内存单元中将其返回)。

提供函数原型:函数原型描述了函数到编译器的接口,将函数的返回值的类型以及参数的类型和数量告诉编译器。

调用函数:传入实参调用函数。

2、在函数中声明的变量

在函数中声明的变量包括参数都是该函数私有的,在函数调用时,计算机为这些变量分配内存,函数结束时,计算机将释放这些变量使用的内存。

3、函数与数组

在将数组作为函数参数传递的时候,既可以用int arr[],也可以用int * arr。

4、指针和const

常量指针和指针常量

方法1:让指针指向一个常量对象,防止该指针来修改所指向的值

1
2
3
4
// 防止pt修改指向的值,但不能防止修改pt指针
// 可以使用age来修改自身的值
int age = 18;
const int * pt = &age;
  • const 变量的地址可以赋给指向const的指针,但是不能赋值给常规指针。

方法2:将指针本身声明为常量

5、使用const的好处

避免由于无意间修改数据而导致的编程错误。

使得函数能够处理const和非const实参,否则将只能接受非const数据。

例:在使用函数来对某一个数组进行显示时,可以在形参使用const修饰数组,以防止函数对原始数组的修改。

第八章 函数探幽

函数更深的应用

1
内联函数

内联函数

常规函数和内联函数之间的区别在于C++编译器是如何将他们组合到程序中。对于常规函数的调用,会使用堆栈记录原始指令地址,随后跳转到目的函数中,执行结束后再返回原指令。对于内联函数的帝国用,编译器将使用相应的函数代码代替函数调用,在此过程并没有函数跳转调用的过程。

相比之下,内联函数少去了函数跳转的时间,使用空间换时间。

宏是通过文本替换来实现的。

引用变量

引用时已定义变量的一个别名。引用必须在声明的时候将其初始化。

引用作为参数传递可以使得被调用的函数访问调用函数中的变量。在传入参数中使用引用,就是为调用函数的变量新建了一个别名。

什么情况下会生产临时变量:

1、实参的类型正确,但不是左值

1
2
3
4
5
左值:可以取地址的,有名字的,非临时的变量就是左值
右值:无法直接取到地址,没有名字的,临时变量就是右值

新增右值引用的目的
实现移动语义

2、实参类型不正确,但是可以转化为正确的类型

使用引用变量的场景

1、程序员能够修改调用函数中的数据对象

2、通过传递引用而不是整个数据对象,可以提高程序的运行速度

函数重载

原来仅仅是返回类型不一样是不能用作函数重载的。重载必须满足的是特征标不同,即传入的参数不一致。

名称修饰:占位符

函数模板:使用泛型来定义函数(通用编程)

template :定义模板以后使用模板定义数据

当使用模板时,在调用模板函数的过程中,编译器会自动生成使用的模板类型的函数版本,但这一个过程对于程序员来说是透明的。

显示具体化

在模板和具体化函数调用匹配产生冲突的时候,将优先使用具体化的函数进行匹配。

显式实例化:通过在函数调用的时候显示的给出需要用到的模板类型,这样编译器在进行编译期间就不需要自己分析模板类型。

编译器选择函数版本过程

1、创建候选函数列表

2、使用候选函数列表创建可行函数列表

3、确定是否有最佳可行函数

decltype:类型推导

decltype(x+y) xpy = x + y

自动推导x+y的数据类型,并将其类型用于定义xpy,适合和模板进行共同使用。

C++11后置返回类型:使用->将返回类型后置

sizeof和strlen之间的区别,指针变量是无法用sizeof统计出字符串长度的。

第九章 内存模块与名称空间

单独编译

1
2
3
4
5
6
7
头文件包含内容
1、函数原型
2、使用#defineconst定义的符号常量
3、结构声明
4、类声明
5、模板声明
6、内联函数

存储持续性、作用域和链接性

数据存储方案

  • 自动存储持续性:函数参数,函数调用期间存在,函数执行结束释放
  • 静态存储持续性:整个程序运行过程中都存在,static变量
  • 线程存储持续性:生命周期和所属线程一样长
  • 动态存储持续性:new存在,delete释放,用户管理。

作用域:名称在文件中的可见范围

连接性:名称如何在不同单元中进行共享

C++中的auto:自动类型推导

变量只能定义一次,但可以多次应用,可以使用extern关键字对外部变量进行引用。

  • 说明符和限定符
    • auto:自动类型推导
    • register:指示寄存器存储
    • static:用在作用域在整个文件的声明中时,表示内部链接性
    • extern:表明是引用声明,声明引用在其它地方的变量
    • thread_local:与线程的关系类似于静态变量于整个程序
    • mutable:可修改的const
  • cv-限定符
    • const
    • volatile:告诉编译器不要进行编译器优化,比如一个for循环计数
  • mutable限定符
    • 即使为const也可以修改

内部链接性意味着每个文件都有自己的一组常量,而不是所有文件共享一组常量。

new定位运算符:能够指定要使用的位置,需要包含头文件new,

名称空间

声明区域:参数在其在进行中所在的区域。

潜在作用域:潜在作用域小于声明的区域。

名称空间可以是全局的,也可以位于另一个名称空间中,但不能够位于代码块中。默认情况下,在名称空间中声明的名称的链接性为外部的。

1
2
3
namespace Jack{
//名称空间用到的变量和函数,类似于封装
}

using 和 using namespace:using可以使得某一个名称空间里面的函数或变量可用,using namespace 使得整个名称空间内的变量和函数都可用。

如果全局中含有某名称空间的同名的变量,引用该名称空间时将自动隐藏名称空间的该变量,而使用全局变量。

第十章 对象和类

面向对象的特性:抽象、封装和数据隐藏、多态、继承、代码的可重用性

抽象和类

接口:接口是一个共享框架,供两个系统交互时使用。

访问控制字:public、private、protected,公有、私有、保护

结构体默认访问类型为public,类的默认访问类型为private

类的构造函数和析构函数

构造函数和析构函数都没有返回值类型

构造函数

显示调用和隐式调用,每一个对象在创建的时候会自动调用构造函数,构造函数可以由用户给出,也可以使用默认的构造函数(不做任何操作)。

构造函数可以存在多个,能使用函数重载的方式来使用不同的构造函数。

析构函数

析构函数是在对象释放的时候自动调用的函数,用于释放新建的对象资源。

  • C++11列表初始化方式
1
2
3
4
5
Bozo(const char * fname, const char * lname);

Bozo bozette = {"Bozetta", "Biggens"};
Bozo fufu{"Fufu", "O'Dweeb"};
Bozo *pc = new Bozo{"Popo", "Le Peu"};

this 指针

this指针指向的是自身的地址,如果需要返回自身对象,需要使用*this,访问自身的成员也可以使用this->成员变量来进行访问。

第十一章 使用类

运算符重载

  • 不能重载的运算符

    sizeof、.(成员运算符)、.*(成员指针运算符)、::(作用域运算符)、?:(条件运算符)、typeid、const_cast、dynamic_cast、reinterpret_cast、static_cast

运算符重载是面向对象多态的一种表现,使用运算符重载能够使得编程更加灵活,例如实现两个对象相加,可以使用+运算符重载。

operator运算符()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Time{
int a;
int b;
public:
Time(int m_a, int m_b){
a = m_a;
b = m_b;
}

// 对 + 运算符进行重载
Time operator+(Time& t2){
Time time;
time.a = a + t2.a;
time.b = b + t2.b;
return time;
}
}

友元函数

友元的三种类型:友元函数、友元类、友元成员函数。

将函数、类、成员函数设置为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

  • 创建友元函数

    将函数原型前面加上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
25
26
27
#include<iostream>
using namespace std;

// 对<<运算符进行重载
class Time {
friend ostream& operator<<(ostream& os, const Time& t);
int hour;
int second;

public:
Time(int m_hour, int m_second) {
hour = m_hour;
second = m_second;
}
};
// 返回流对象,便于传递操作
ostream& operator<<(ostream& os, const Time& t) {
os << t.hour << " " << t.second;
return os;
}

int main() {
Time time(2, 40);
cout << time << endl;
system("PAUSE");
return 0;
}

当重载的函数和目标对象不在一个作用域内时,需要使用域名控制符号来对其进行访问。

运算符重载,还可以更具特征标的数量不同再次进行重载。

类型转换

构造函数可以使用隐式转换:隐式构造函数必须不能存在二义性

在下面程序中,使用Stonewt st = 12;将默认隐式调用了构造函数Stonewt(double lbs),对Stonewt的成员进行初始化。

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
#include<iostream>
using namespace std;

class Stonewt {
int stone;
double pds;
double pounds;

public:
Stonewt(double lbs) {
stone = int(lbs);
pds = int(lbs) % 14 + lbs;
pounds = lbs;
}
Stonewt() {
stone = pds = pounds = 0;
}
void show() {
cout << stone << endl;
cout << pds << endl;
cout << pounds << endl;
}
};

int main() {
Stonewt st = 12;
st.show();

system("PAUSE");
return 0;
}

如果关闭隐式构造函数则使用:explicit Stonewt(double lbs);

转换函数

格式:operator typename();

exp:operator double();

如果在类里面定义了转化函数,可以将类强制转化成其它的数据类型。转化函数中返回需要进行转化的数据。

第十二章 类的动态内存分配

动态内存和类

再动态分配内存中,对象的析构函数是必不可少的,有的时候必须要重载赋值运算符。

在类里面定义静态成员,意味着该类的所有对象能够共同享用这同一个静态成员变量。

将对象作为函数参数来传递而不是使用引用来传递,容易造成函数结束以后对象自动释放并调用析构函数的现象。

当使用一个对象初始化另一个对象的时候会调用拷贝构造函数。

  • C++为一个对象自动提供的成员函数:

    • 默认构造函数:在对象创建时进行调用
    • 默认析构函数:在对象销毁时调用
    • 复制构造函数:用于将一个对象复制到新创建的对象中,默认复制构造函数用到的是浅拷贝。

    • 赋值运算符重载:通常将一个对象使用等号赋值给另一个对象会用到,默认用的时浅拷贝

    • 地址运算符重载
    • 移动构造函数
    • 移动赋值函数
  • 浅拷贝和深拷贝

    在使用复制构造函数的过程中,默认进行的是浅拷贝,将一个对象的地址赋值给另一个需要初始化的对象。通过显性的修改复制构造函数的拷贝过程,可以实现深拷贝,即重新申请一块内存空间,将一个对象的数据放进去,让两个对象使用不同的两个地址空间。

  • C++11中引用nullptr表示空指针

  • 可以将成员函数声明为静态的,声明过后

    • 不能通过对象调用静态成员函数,静态成员函数不能使用this指针
    • 静态成员函数只能够使用静态数据成员

使用new注意事项

  • 使用new后应该同样用delete进行释放
  • new和delete必须相互兼容,new对应于delete,new[]对应与delete[]
  • 如果有多个构造函数,必须以相同的方式使用new
  • 应定义一个复制构造函数,深拷贝一个对象初始化为另一个对象
  • 应定义一个等号=运算符重载,通过深拷贝将一个对象赋值给另一个对象

有关返回对象的说明

  • 返回对象的引用、返回指向对象的const引用和返回const对象的区别

    • 1、返回对象将调用赋值构造函数,返回引用不会
    • 2、引用指向的对象应该在调用函数执行时存在
  • 返回指向非const对象引用

    非const则说明可以对对象进行修改,具有传递性的操作必须返回非const对象引用

  • 当返回对象是被调用函数当中的局部变量,则不应该以引用来返回,而应该使用对象来返回,调用拷贝构造函数创建一个新的对象,比如算数运算符。

若方法要返回局部对象,则应返回对象

若方法或函数要返回一个没有共有复制构造函数的类的对象,必须返回一个指向这种对象的引用

第十三章 类继承

通过继承可以完成的一些工作:

1、可以在已有类的基础上添加功能

2、可以给类添加数据

3、可以修改类的方法

总结,对于父类的一些功能的拓展。一般情况下会遵守开闭原则。

派生的类型:公有派生、私有派生、保护派生

公有派生:基类的公有成员称为派生类的公有成员。基类的私有部分称为派生类的一部分,但是只能够通过基类的公有和保护方法来进行访问。

父类指针指向子类对象, 通过这样的方式可以实现多态

基类指针可以在不进行显示类型转换的情况下指向派生类对象;

基类引用可以在不进行显示类型转化的情况下引用派生类对象;

多态公有继承

方法1、在派生类中重新定义基类的方法,通过在父类对象和子类对象中定义同样的方法,分别使用父类和子类对象的实例来调用来实现不同对象对同一对象接口调用达到多态。

方法2、使用虚方法,使用父类指针指向子类对象,在父类对象中使用虚函数,在子类对象中实现,通过一个父类指针指向子类对象来调用子类中的函数。

  • 虚析构函数的作用
    • 使用虚析构函数可以确保正确的析构函数序列被调用。

动态联编和静态联编

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。

在编译过程中进行的联编称为静态联编。

在程序运行时进行的联编称为动态联编,有虚函数的代码需要在函数运行过程中才能够确认虚函数中具体要执行和完成的任务。

虚函数的工作原理

编译器处理虚函数的方法是,为每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。

基类对象中包含一个指针,指向基类中所有虚函数表的地址表。派生类对象将包含一个指向对立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有定义虚函数,该虚函数表将保存函数原始版本的地址。

访问控制:protected

派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员,对于外部世界来说,保护成员的行为与私有成员相似,对于派生类来说,保护车关于的行为与公有成员相似。

抽象类

C++可以通过纯虚函数提供未实现的函数,纯虚函数声明的结尾处为=0。

当类声明中包含纯虚函数时,则不能够创建该类的对象。因为需要通过继承的方式来实现纯虚函数中未定义的方法实现。

继承和动态内存分配

1、如果在派生类中没有用到动态内存分配,则无需进行操作,析构函数也不需要修改。

2、如果派生类中有定义的指针需要动态分配内存的成员变量,则必须为派生类定义显式析构函数、复制(拷贝)构造函数和赋值(=重载)构造函数,在派生类的赋值构造函数和赋值构造函数中还必须显示的调用基类的构造函数。

在派生类中重定义基类的方法不是重载,将直接覆盖基类的原始方法。