c++_learning-基础部分

文章目录

基础认识:

语言特性(面向对象编程):

c++的类(相当于c中的结构体):

  1. 定义类的过程,也被称为定义对象的过程
  2. 类,可以像结构体一样定义成员变量,还可以定义该类的函数(方法)
  3. 把功能包在类中,需要时通过定义一个对象来调用程序,即基于对象的程序设计
  4. 继承性(继承父类后,可以增加新的方法)、多态性,升华了基于对象程序设计,故称为面向对象程序设计。
  5. 易扩展、易维护、模块化,通过设置各种级别来限制访问,维护数据安全

三大特性:

  1. 封装:数据和代码捆绑在一起,避免外界干扰和不确定性访问,封装可以使代码模块化。
  2. 继承:可以通过继承父类的数据和方法,也可以新增、修改继承来的方法(重写和重载),从而提高程序的复用性。
  3. 多态:就是让具有继承关系的不同的类对象,可以调用同名的成员函数,并产生不同的响应结果,即多态的目的是接口重用。
    • 静态多态:编译期,函数重载
    • 动态多态:运行期,虚函数重写

c++包含四种编程范式:

面向过程、面向对象、泛型编程、函数式编程(lambda表达式)。

优缺点:

优点:具有强大的抽象封装能力、高性能、低功耗。c++相比于C语言,有类、虚函数、标准库

缺点:语法相对复杂,学习曲线比较陡;需要一些好的规范和范式,否则代码很难维护。

c++程序编译的过程:预处理->编译(优化、汇编)->链接

编译型语言->可执行程序:

  1. c++要生成一个可执行文件,需要将 .cpp 经过编译、链接。
  2. 每个 .cpp 文件,经过编译后对应一个.obj文件(linux对应的是.o文件),将各个.obj文件链接起来就是.exe可执行文件。

源代码的组织:

  1. 头文件.h:#include头文件、函数声明、结构体声明、类声明、模板的声明和定义、内联函数、#defineconst定义的常量等。
  2. 源文件.cpp:#include<***.h>头文件、函数的定义、类定义。
  3. 主程序main:#include需要的头文件,实现主程序和框架。

生成可执行文件的步骤:

预处理: 头文件展开、去注释、宏替换、条件编译等;

预处理的指令有三种,包含的头文件,#include;宏定义,#define(定义宏)、#undef(删除宏);条件编译,#ifdef#ifndef

包含的头文件,#include:
  • #include<...>:直接从编译器自带的函数库的目录中寻找文件。
  • #include"...":先从自定义的目录中寻找文件,找不到再从编译器自带的函数库目录中寻找。

注意:编译器会将头文件的内容,复制到包含头文件的文件中。

宏定义,#define:

编译时,编译器会将程序中的宏名用宏内容替换,即宏展开

  1. 无参数的宏:#define 宏名 宏内容

  2. 有参数的宏:#define Max(x,y) ((x)>(y) ? (x):(y))

    c++中,内联函数inline可以替代有参数的宏,且效果更好。

  3. c++ 中常用的宏:

    _FILE_:当前源代码的文件名,即绝对路径
    _FUNCTION_:当前源代码的函数名
    _LINE_:当前源代码的行号
    _DATE_:编译日期
    _TIME_:编译时间
    _TIMESTAMP_:编译时间戳
    _cplusplus:c++程序编译时,该宏就会被定义

条件编译,#ifdef、#ifndef:
#ifdef 宏名     // 如果宏名存在,则执行程序段一,否则执行程序段二
    程序段一
#else
    程序段二
#endif


#ifndef 宏名     // 如果宏名不存在,则执行程序段一,否则执行程序段二
    程序段一
#else
    程序段二
#endif

在c++使用预编译指令#include时,为了防止头文件重复包含(即头文件防卫式声明),两种方式:

  1. #ifndef指令:受c++语言标准的支持,可以针对文件中的部分代码;
  2. #pragma once指令放在文件开头:有些编译器不支持,只能针对整个文件,但效率更高;

注意:这种方法仅仅对单个.cpp文件有效,不是整个项目,即只是在编译时防止了重定义。但可能出现链接时的重定义

编译(只有源文件.cpp才能编译):

将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制文件)。

链接:

将编译生成的目标文件,以及他们所需要的库文件链接起来,生成可执行文件。

更多细节:

  1. 分开编译的优点:每次只编译修改过的源文件,然后再链接,效率更高;

  2. 编译单个.cpp文件只需知道所用到的变量/函数/类的名称的存在即可,不会将它们的定义一起编译;

    如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的错误;

  3. 链接时,变量、函数和类的定义只能有一个,否则会出现重定义的错误;

    如果把变量、函数、和类的定义放在.h文件中,.h被多次包含,链接前会存在多个副本在不同的.cpp文件中,则链接时会出现重定义错误

    如果将变量、函数、类的定义放在.cpp文件中,.cpp文件只会被编译一次,链接前不会重复包含,故不会报错;

  4. 尽可能不使用全局变量,如果一定要使用,需要在.h文件中声明且要加 extern 关键字,在.cpp文件中定义

    全局的 const 变量在头文件中定义,且const 变量仅仅对单个文件内有效

    全局 const 变量和全局变量的区别?

    • 作用域不同:

      1)全局 const 常量只对本文件内有效。

      2)全局变量对所有 #include 头文件的文件有效。

    • 定义方式不同:

      1)全局变量需要在.h文件中声明并加extern关键字,在.cpp文件中定义。

      2)const全局常量直接在本文件中声明和定义。

  5. 到底怎么样才能避免重复定义呢?

    关键是要避免重复编译 ,防止头文件重复包含是有效避免重复编译的方法,即不要将同一个.h文件在多个文件中#include。

    但最好的方法还是: 头文件尽量只有声明,不要有定义。这么做不仅仅可以减弱文件间的编译依存关系,减少编译带来的时间性能消耗,更重要的是可以防止重复定义现象的发生,防止程序崩溃。

  6. 函数模板 和 类模板的声明和定义,要放在同一个.h文件中

    函数模板 和 类模板的特化版本的代码,是真实的定义,要放在.cpp文件中

计算机体系中的存储层级:

在这里插入图片描述

内存:

  1. 能存储的比特数,取决于集成电路里的元器件的数目。

  2. 内存中的资源,会被操作系统进行调用,分配给正在执行的程序
    1)操作系统会给自己预留一部分内存资源;
    2)其余的由其他正在执行的程序进行分配;

  3. 程序只能在操作系统分配给它的范围内使用内存:

在这里插入图片描述

  • 全局变量、程序代码,分配在静态内存区域,即从开始到结束这些内存区域都被占用;

  • 程序在运行时,可以向操作系统动态的申请和释放一些内存(堆内存)。

  • 局部变量、函数参数返回值等,被分配在栈内存区域,即函数调用栈

    函数每一次被调用时,在函数调用栈中分配一个大小合适的栈帧(存储这一次的局部变量、参数和返回值)。在函数返回时,释放栈帧的内存。

    注意:递归过深会导致程序崩溃,是因为大量的栈帧未释放,占满了函数调用栈的内存,即stack overflow

在这里插入图片描述

堆、栈的不同用途和区别:

不同用途:

  • 栈:空间有限,编译器自动分配,速度较快。
  • 堆:只要不超过实际的物理内存,而且在操作系统能分配的最大内存大小内,都可以分配;分配速度慢;通过malloc/free、new/delete来实现。

区别:

  1. 管理方式不同:栈是自动管理的,出作用域将被释放;堆需要手动释放,否则可能引发内存泄漏;
  2. 空间大小不同:堆的空间大小受限于物理内存空间;栈就小的可怜只有8M(可修改系统参数);
  3. 分配方式不同:堆是动态分配的,需手动释放;栈是静态分配和动态分配,但都是自动释放;
  4. 分配效率不同:栈是系统提供的数据结构,由计算机底层支持,进出栈有专门的指令,效率较高;堆是由c++函数库提供的;
  5. 是否产生碎片:栈是严格按照(先进后出LIFO)顺序,不会产生碎片;堆频繁的随意分配和释放,会造成内存空间的不连续故容易产生碎片,太多碎片会导致性能下降;
  6. 增长方向不同:栈向下增长,以降序分配内存地址;堆向上增长,以升序分配内存地址;

动态内存分配的注意事项:

  • 动态分配的内存没有变量名,只能通过指向它的指针来操作内存中的数据;

  • 实现:

    1)C语言:通过malloc/free,从堆区申请和释放内存

    void* malloc(int NumBytes)
    // NumBytes:是要分配的字节数
    // 分配成功,返回指向被分配内存的指针,即返回一个地址;分配失败,返回空地址NULL
        
    // 当不使用这段内存时,要用free函数,将这段内存释放并被系统回收,需要时再重新分配
    void free(*FirstBytes)
    

    eg. 给申请的100个整型内存空间赋值:

    // 分配400个字节
    int *ptr = (int *)malloc(100*sizeof(int));
    if (ptr != NULL)
    {
        // 通过指针ptr1,给指向ptr的内存空间赋值
        int *ptr1 = ptr;
        for (int i = 0; i < 100; i++)
        {
        	*ptr1++ = int(i);      // 等价于*(ptr1 + i) = i;
        }
        
        // 输出申请的100个整型内存空间的值
        if (ptr1 != nullptr)   // NULL和nullptr实际上是不同的类型;尽量在涉及指针时,能用nullptr就用
        {
            for (int i = 0; i < 100; i++)
            {
            	cout << *(ptr + i) << " ";      
            }
            cout << endl;
        }
    
        // 释放申请的内存
        // ptr = NULL;
        // delete ptr;
        free(ptr);
    }
    

    2)c++:用new/delete(运算符(标识符))分配和释放在堆区的内存

    // new使用的一般格式:
    指针变量名 = new 类型标识符;
    指针变量名 = new 类型标识符(初始值);
    指针类型名 = new 类型标识符[内存单元的个数];
    
    // delete使用的一般格式:
    new的时候,用[ ]delete就必须加[ ](不用写数组的大小); 
    

    注意:如果动态分配的内存不再使用了,必须delete释放它,否则可能耗尽系统的内存。

可移植性:

  1. 编译性语言:编译为二进制文件(可执行文件),执行速度快。

    1)先将源文件逐个编译compile为.obj二进制目标文件,链接link后,生成二进制.exe可执行文件。

    2)源程序 -> 编译器 -> 目标程序 -> 链接器 -> 可执行程序
    在这里插入图片描述

  2. 解释性语言:不进行编译,先解释再运行,如python。

进程在内存空间中的布局:

当可执行文件被加载到内存之后,就变成了一个进程。

进程的虚拟地址空间:

  • 栈(堆栈/栈区)(地址由高向低生长):局部变量(每次执行程序时该变量的地址都会发生变化),编译时期即可确定变量的范围,作用域是{}

    windows系统默认的栈区的大小是1M、Linux默认的栈区的大小是8M / 10M

  • 堆区(地址由低到高生长):new、malloc等申请的内存空间,需要在运行阶段才能确定变量大小的范围,作用域是整个程序范围内

    所有系统的堆空间的上限:接近内存(虚拟内存)的总大小的(除了一部分被OS占用)。

  • 数据段:全局变量(已初始化的全局变量和BSS段(未初始化的全局变量))、静态成员变量、全局函数的入口地址。

    一些全局量(全局变量、全局函数、类静态成员变量等)的地址值在生成可执行文件时,已经确定好了,不会改变;存放在bss段、数据段等,一旦加载(映射)到内存时,这些地址值都不会发生变化;

  • 代码段:存放程序执行代码的一块内存区域。

API:

操作系统预先把这些复杂的操作写在一个函数里面,编译成一个组件(一般是动态链接库),随操作系统一起发布,并配上说明文档,程序员只需要简单地调用这些函数就可以完成复杂的工作。

这些封装好的函数,就叫做API(Application Programming Interface),即应用程序编程接口。

  • C语言 API 以函数的形式呈现。
  • C++ 是在C语言的基础上进行的扩展,所以 C++ API 既包含函数也包含类。
    在这里插入图片描述

基础语法:

命名空间namespace{...}

作用:为了防止名字冲突而引入的一种机制。

命名空间分割了全局空间,每个命名空间可以看作一个作用域,可以在不同的命名空间中定义同名的类、函数、模板、变量等。

命名空间的定义,可以不连续,甚至可以在多个文件中;可以在同一个或不同的.cpp文件中,通过打开namespace,添加新的成员函数。

命名空间中,类、函数、模板、全局变量等的分文件编写,与不使用命名空间的做法相同。

调用格式:

// 1、在同一个.cpp文件中
namespace  命名空间名
{
    // 类、函数、模板、变量的声明和定义
}

命名空间名::实体名

// 2、在不同的.cpp文件中
using namespace 命名空间名;

命名空间名::实体名

注意:

  1. 命名空间中声明全局变量,而不是使用外部全局变量和静态变量;

  2. 对于using声明,首选将其作用域设置为局部而不是全局;

    namespace 
    {
        int a = 10;
    
    }
    
  3. 不要在头文件中使用#using编译指令,非要使用,应该其放在所有的#include之后;

  4. 匿名命名空间,从创建的位置到程序结束,都是有效的,且仅仅可以在当前文件中(直接)使用;

    namespace 
    {
        int a = 10;
    
    }
    
    int main()
    {
        cout << a << endl;
    }
    

常用的数据结构及其内存分配:

  1. 变量:一块具有类型的内存(类型:数据存储的表示方式,以及你可以对它进行的操作);
  2. 指针:一个内存的地址(指针的类型,可能说明该指针指向的特定类型的变量;void*可以指向任何特定类型的变量);
  3. 引用:可以理解为一种“语法糖”(左值引用/右值引用);
  4. 数组:内存中连续排列的多个同类型变量,数组名称可以作为指向第一个元素的指针;
  5. 自定义类型(class/struct):一组成员变量在内存里的排列方式,以及可以对它进行的操作;
  6. 对象:按照特定排列方式,存储在内存里的一组成员变量;

变量与数据类型:

变量是在程序执行过程中可以改变的量,即代表一块内存区域,修改变量值会引起内存区域中内容的改变

变量名:标识内存中的一个具体的存储单元,即地址,方便操作这段内存

数据类型,决定变量分配空间的大小。

基本数据类型:

整型:

有符号整型:short(2 bytes)、int(4 bytes)、long(4 bytes)。

  • 机器数 != 真值(补码形式)

    -3:

    机器数:10000000 00000000 00000000 00000011

    真值:11111111 11111111 11111111 11111101

    3:

    机器数:00000000 00000000 00000000 00000011

    真值:00000000 00000000 00000000 00000011

  • 补码形式:

    负数的补码:正数的补码 -> 按位取反后+1;

    正数的补码还是正数;
    在这里插入图片描述

无符号整型:unsigned short、unsigned int、unsigned long。

浮点型:

实型 float 4bytes 、双精度 double 8bytes

字符型:
字符char:

用单引号引起来的一个字符,如字符型常量 ‘a’(占用一个字节,存放 a)。

转义字符:‘\\n’ 、‘t’、 ‘\\’。

char []char*的区别:

  1. 地址和地址存储的信息;
    char* str = "hello world",指向的是字符串常量,会存储在全局区。

    char str[11] = {"hello world"},存储在栈区。

  2. 可变和不可变:

    char*指向的常量可以改变,但常量中的内容不能改变,具体的还要看char*指向的存储区域是否可变
    char []中的内容可以改变,但整体变量不能改变。

c风格的字符串:
#define CRT_SECURE_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;

struct Stu
{
	char* name;	
};
int main()
{
	Stu stu;
	// 使用memset函数,将stu.name=nullptr置空
	memset(&stu, 0, sizeof(Stu));  
	
	stu.name = new char[21];
	char* name = (char*)"yoyoll";
	strncpy(stu.name, name, sizeof(name));
	cout << stu.name << endl; 
	
	delete[] stu.name;
	stu.name = nullptr;
	
	return 0;
}

c语言中,如果字符型char数组的末尾包含了空字符’\0’(即0),那数组中的内容就是一个字符串。

在这里插入图片描述

由于字符串必须以'\0'结尾,故声明时要预留1个字节的位置,如char str[21]只能存放20个字符。

// 清空字符串:void* memset(void* buffer, int ch, size_t count);
char name[20];   
memset(name, 0, sizeof(name));  // 会将字符串name中的所有字符置为0,即字符'\0'

// 字符串的复制或赋值:
char* strcpy(char* dest, const char* src);   // 将src指向的字符串拷贝到dest所指的地址,复制完字符串后,在dest尾加'\0'
// 注意:如果dest指向的内存空间不够大,则会导致数组越界
char* strncpy(char* to, const char* from, size_t count );  // 将 字符串from 中至多count个字符复制到 字符串to
// 如果 字符串from 的长度小于count,其余部分用'\0'填补;长度大于count,则只会截取前count个字符,且不会在dest后追加'\0'

// 获取字符串的长度:
size_t strlen(const char* str);
// 区分:strlen(str)返回字符串str的字符数,而sizeof(str)返回字符串str的字节数。

// 字符串的拼接:
char* strcat(char*dest, const char* src); // 注意:如果dest指向的内存空间不够大,则会导致数组越界
char* strncat(char* dest, const char* src, const size_t n);

注意:

  1. 处理字符串时,会从起始位置开始搜索,直到找到’\0’即0为止,不会判断是否越界(因大部分函数用char*作为字符串的形参,故无法获取字符串的长度,只知道字符串的起始地址和其以’\0’结尾);

  2. 字符串每次使用前都要初始化,三种初始化的方式导致的不同:
    char* constPtr = "hello",ptr是一个字符串指针,"hello"被存放在常量区,不可修改;

    char charArr[] = "hello",charArr是一个字符串数组,存放在栈区,可修改;

    char* charPtr = (char*)malloc(sizeof(6)); strcpy(charPtr, "hello"); ,即"hello"被存放在堆区,可修改;

  3. VS中,如果要使用c标准的字符串操作函数,要在源代码前加#define _CRT_SECURE_NO_WARNINGS

string不是基本数据类型:

c++中string类是封装了c风格的字符串:c++的字符串string中有一个指向动态分配的内存地址指针

c++11中的原始字面量,可以直接表示字符串的实际含义,且不需要转义和连接;语法:R"(字符串的内容)"R"***(字符串的内容)***"

注意:

  1. Visual Studio中,未初始化的栈空间用0xCC填充,而未初始化的堆空间用0xCD填充。
  2. 0xCCCC0xCDCD在中文GB2312编码中分别对应“烫”字和“屯”字。
  3. 如果一个字符串没有结束符’\0’,输出时就会打印出未初始化的栈或堆空间的内容,就会出现“烫烫烫”、“屯屯屯”乱码。
关于字符的表示问题,即将字符与相应的数字对应起来:
  • ASCII码:

    1)基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。

    2)使用指定的7或8位二进制数组合成的0127和0255的十进制数字,表示可能的字符。

  • Unicode编码

    最初的目的是:将世界上的文字都映射到一套字符空间中,并转化成相应的数字存储起来

    为了表示Unicode字符集,有3种(确切的说是5种)Unicode的编码方式:

    1. UTF-8:

    1)1 byte表示一个字符,可以兼容ASCII码

    2)特点:存储效率高,变长(不方便内部随机访问),无字节序问题(可作为外部编码)

    1. UTF-16:

    1)2 bytes表示一个字符,有 UTF-16BE(big endian)、UTF-16LE(little endian)

    2)特点:定长(方便内部随机访问);有字节序的问题(不可作为外部编码)。

    1. UTF-32:

    1)4 bytes表示一个字符,有UTF-32BE(big endian)、UTF-32LE(little endian)

    2)特点:定长(方便内部随机访问);有字节序的问题(不可作为外部编码)。

  • 编码错误的根本原因:编码方式和解码方式的不统一

布尔类型 bool:

1bytes

无值型 void:

0bytes

非基本数据类型:

数组 type[ ]:
动态创建数组:
  1. 使用new 数据类型[]动态创建数组时,需要用delete[] 数组名,来释放动态分配的内存空间。

  2. new分配内存时,如果内存不足,会报错导致程序中止。

    在new关键字后添加std::nothrow选项后,则返回的是nullptr,并不会产生异常。

    在这里插入图片描述

  3. delete[]中,不需要指定数组的大小,系统会自动跟踪已分配数组的内存。

  4. 声明数组时,如果数组的长度是变量,相当于在栈上动态分配数组,并且不需要释放。

一维数组:

初始化:

数据类型 数组名[大小]={ val1, val2, ... }
数据类型 数组名[大小]={ 0 };  // 初始化所有变量为0

数组的本质:

  • 数组一段连续的内存空间,且数组名表示该段连续内存的首地址,即数组第0个元素的地址
  • 指针的值是可以修改的(除了常量指针和常量常指针),但数组名是常量,不可修改

数组的指针表示法:

  • c++编译器的解释:地址名[下标],即(地址名+下标)

  • 数组名[下标],即*(数组名+下标)

    举例:(&arr[2])[2] --> arr[4]char arr[10]; char* ptr = arr; cout << *(ptr + i) << endl;

// 清空数组:(最常用来初始化清空一个字符串)
void* memset(void* s, int val, size_t bytes_num);

// 拷贝数组:
void* memcpy(void* dest, void* src, size_t bytes_num);

// 数组的排序qsort:(快速排序)
void qsort(void *base, int nelem, int width, int (*fcmp)(const void* p1, const void* p2));
/*
qsort函数中,第四个参数回调函数决定了排序的顺序:
	返回值 < 0,p1会排在p2的前面;
	返回值 == 0,p1和p2的顺序不确定;
	返回值 > 0,p2会排在p1的前面;
注意:回调函数中的void*必须具体化,即转化为具体的数据类型才能使用。

qsort()函数中,为什么要传入第三个参数?
答:因为qsort不知道数数组的具体类型,故在“回调函数内部是通过内存块操作数据”的,交换两个数据是通过memcpy()函数实现的,而不是数据类型。
*/

#include <iostream> 
#include <stdio.h>
#include <stdlib.h>
using namespace std;

void Print(int* ptr, size_t size)
{
	if (ptr == nullptr) { return; }
	for (int i = 0; i < size; ++i)
	{
		cout << *(ptr + i) << ",";
	}
	cout << endl;
}

/*
    返回值 < 0,p1会排在p2的前面
    返回值 > 0,p2会排在p1的前面
    返回值 == 0,p1和p2的顺序不确定
*/
int cmpAsc(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
int cmpDesc(const void* p1, const void* p2)
{
	return (*(int*)p2 - *(int*)p1);
}

int main(int argc, char *argv[])
{
	int arr[10] = { 1,4,5,0,2,9,3,7,6,8 };
	qsort(arr, sizeof(arr) / sizeof(int), sizeof(int), cmpAsc);
	size_t size = sizeof(arr) / sizeof(int);
	Print(arr, size);
	qsort(arr, sizeof(arr) / sizeof(int), sizeof(int), cmpDesc);
	Print(arr, size);

	return 0;
}

在这里插入图片描述

二维数组:

在内存中,是以行优先的形式,存放在连续的内存空间中的。

可用一维数组的方法查看二维数组,只需二维数组的首地址和大小即可。


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

int main()
{
	int m = 2; int n = 3;
	int arr[m][n];
	memset(arr, 0, sizeof(arr));
	arr[0][2] = 1; arr[1][2] = 2;
	int* ptr = (int *)arr;
	for (size_t i = 0; i < 6; ++i)
	{
		cout << *(ptr + i) << ",";
	}

	return 0;
}

二维数组用于函数形参列表:

// 行指针:
数据类型 (*行指针名)(行大小) = &一维数组名;    // 行大小即数组长度;&一维数组名,即数组的地址,也是行地址
int arr[2][3];    
int(*p)[3] = arr;   // arr是二维数组的首地址,即0号元素的地址      

// 将二维数组传递给函数:
void func(int(*p)[3], ...);
void func(int p[][3], ...);
三维数组:
int arr3D[2][3][4];
memset(arr3D, 0, sizeof(arr3D));

int (*p)[3][4] = arr3D;
void func(int(*p)[3][4], ...);
指针 type *:
指针变量:

简称指针,是一种特殊的变量,专用于存放变量在内存中的起始地址。

语法:数据类型 *变量

对指针的赋值:

  • 任何数据类型的地址都是以十六进制存储在内存中的
  • 指针变量 = &变量
  • 不同的指针存放不同类型变量的地址

指针占用的内存:

  • 指针也是变量,故需要占用内存
  • 64位操作系统中,指针变量占用的都是8 bytes
  • 指针存放变量的地址,指针名表示的就是该地址(就像变量名表示变量的值一样)
  • *解引用,用于指针可以获取该地址中的值。

使用指针的两个目的:

// 传递地址:
int* p;              // 整型指针
int* p[3];           // 一维整型指针数组,元素是3个整型指针p[0]、p[1]、p[2]
int(*p)[3];          // 一维整型数组指针,用于指向数组长度是3的整型数组
int* p();            // 返回值类型是整型的,函数p的地址
int(*p)(int, int);   // p是函数指针,函数返回值是整型int

// 存放动态分配的内存地址:
int* p = new int(3);
二级指针:

指针用于存放普通变量的地址,二级指针用于存放指针变量的地址。

#include<iostream>
using namespace std;

int main() 
{
    int* p = 0;
    {
        int** pp = &p;
        *pp = new int(3);
        cout << pp << ", " << *pp << endl;
    }
    cout << p << ", " << *p << endl;
    
    return 0;
}
空指针:

声明指针后,赋值前,指针指向空,即没有任何地址。

对空指针进行解引用,程序会崩溃。

  1. 函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
  2. 为何访问空指针会出现异常?
    • NULL指针分配的分区,范围是0x00000000 ~ 0x0000FFFF,该段是空闲的空间且没有相应的物理存储器与之对应。
    • 对该段的空间的任何操作,都会引发异常。
    • 需要人为的划分一个空指针区域,即NULL指针分区。

对空指针使用delete运算符,系统会忽略该操作,不会出现异常。内存被释放后,应将该指针指向空。

注意:c++11建议用nullptr表示空指针也就是(void*)0,NULL当作0使用。

野指针:

野指针指向的是非有效的地址,故访问的时候程序可能会崩溃。

出现野指针的情况主要有三种:

  1. 指针在定义的时候,如果没有初始化,它的值是不确定的(乱指的),故如果指针初始化时,不知道指向哪,就指向nullptr。
  2. 如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但指向的地址是无效的,故动态分配的内存被释放后需要将其置空nullptr。
  3. 指针指向的变量已超越变量的作用域,即变量的内存空间已经被系统回收,故函数不要返回局部变量的地址。

野指针的危害比空指针大,故需要避免,否则会造成程序的不稳定。

函数指针:

函数的二进制代码放在内存分区的代码段,函数的地址是其在内存中的首地址。

使用函数指针步骤:

// 声明函数指针:
int(*funcPtr)(int, int)

// 让函数指针指向函数的地址:
int maxValue(int val1, int val2) { return (val1 > val2 ? val1 : val2) };
funcPtr = maxValue;

// 通过函数指针调用函数:
int res = funcPtr(5, 1) // 或 (*funcPtr)(5,1);

主要用于:给函数传递函数指针作为参数,并在函数内部使用该函数指针,达到调用该函数的目的。

#include <iostream>
using namespace std;

template <typename T>
bool ascending(T x, T y) 
{
    return x > y; 
}

template <typename T>
bool descending(T x, T y) 
{
    return x < y;
}

template<typename T>
void bubblesort(T* a, int n, bool(*cmpfunc)(T, T)=ascending){
    bool sorted = false;
    while(!sorted)
    {
        sorted = true;
        for (int i=0; i<n-1; i++)
        {
            if (cmpfunc(a[i], a[i+1])) 
            {
                std::swap(a[i], a[i+1]);
                sorted = false;
            }
        n--;
    }
}

int main()
{
    int a[8] = {5,2,5,7,1,-3,99,56};
    int b[8] = {5,2,5,7,1,-3,99,56};

    bubblesort<int>(a, 8, ascending);

    for (auto e:a) { cout << e << " " };
    cout << endl;

    bubblesort<int>(b, 8, descending);

    for (auto e:b) { cout << e << " " };

    return 0;
}
// -3 1 2 5 5 7 56 99 
// 99 56 7 5 5 2 1 -3  
引用 type &:

使用指针存在的问题:空指针、野指针、容易改变指针指向的值却在继续使用。

引用是c++新增的复合类型,是指针常量(不允许修改指针的指向)的伪装:“引用int& ra = a <==> 指针常量int* const rb = &a”。

int x1 = 2;
int x2 = 3;

// 引用使用时,必须初始化,而且一个引用永远指向它初始化的那个对象
int& x3 = x1;
cout << x1 << "," << x3 << endl;

x3 = x2;
cout << x1 << "," << x3 << endl; 
  • 使用引用,则不存在空引用、必须初始化、一个引用永远指向它初始化的那个对象。
  • 引用,可以认为是变量别名,修改引用的值同时也会改变原变量的值。

函数传递参数的说明:

  • 对内置基础类型而言,在函数中传递时pass by value更高效;
  • 对面向对象中自定义类型而言,在函数传递中pass by reference to const更高效;

疑问:

  • 有了指针,为什么还需要引用?为了支持运算符重载。
  • 有了引用,为什么还需要指针?为了兼容c语言。
创建引用的语法:

数据类型& 引用名 = 原变量名

  • 引用名 和 原变量名的数据类型、值、内存单元相同;
  • 必须要在声明引用的时候初始化,且初始化后不可改变;
引用用于函数的参数:
#include <iostream>
#include <string>
using namespace std; 
 
void funcByValue(int age, string name)
{
	age = 21;
	name = "wowo";
}
void funcByQuote(int& age, string& name)
{
	age = 21;
	name = "wowo";
}
void funcByAddr(int* age, string* name)
{
	*age = 21;
	*name = "wowo";
}

int main()
{
	int age = 10;
	string name = "yoyo"; 
	cout << age << "," << name << endl;
	funcByValue(age, name);
	cout << age << "," << name << endl;
	
	funcByQuote(age, name);
	cout << age << "," << name << endl;
	funcByAddr(&age, &name);
	cout << age << "," << name << endl;
	
	return 0;
}
  1. 把函数的形参声明为引用,调用函数时,形参将是实参的别名,该方法称为引用传递。

  2. 引用的本质是指针常量,传递过程中,传递的是变量的地址,故函数中对形参的修改会影响实参。

  3. 传值、传地址、传引用相比,引用传递的优点:

    1)传引用更简洁,且避免了不必要的值拷贝;

    2)引用传递避免了二级指针;

    #include <iostream> 
    using namespace std; 
     
    void funcByQuote(int*& p)
    {
    	p = new int(3);
    	cout << *p << endl;
    }
    void funcByAddr(int** p)
    {
    	*p = new int(3);
    	cout << **p << endl;
    }
    
    int main()
    {
    	int* p = nullptr;
    	funcByQuote(p);
    	funcByAddr(&p);
    	
    	return 0;
    }
    
  4. 引用传入函数的形参用const修饰:

    作用:

    1. 引用为const时,c++将创建临时变量,并让引用指向临时变量。何时创建临时变量?

      1)引用的数据对象类型不匹配:c++会创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量;

      2)引用的数据对象类型匹配,但不是左值;

      const int& val = 8;
      // 等价于
      int tmp = 8;
      const int& val = tmp;
      
    2. 如果不想函数修改引用传入的实参,可以在形参列表中数据类型前加const修饰;

    void funcByQuote(const int& val1, const int& val2);
    

    原因:

    1)使用const,可以避免函数中无意修改数据而造成的错误;

    2)使用const,函数能正确的生成临时变量;

    3)使用const,函数就能够处理const和非const实参,否则只能接受非const实参;

引用用于函数返回值:
  • 如果返回局部变量的引用,本质上是野指针;

  • 可以返回函数的引用形参、类的成员、全局变量、静态变量;

    #include<iostream>
    #include<string>
    using namespace std;
    
    struct Stu
    {
        string name;
        int age;
    };
    
    // 返回函数的引用形参
    ostream& operator<<(ostream& out, const Stu& stu)
    {
        out << stu.name << ":" << stu.age << endl;
    	return out;
    }
    
    int main()
    {
        Stu stu{"wowo", 12};
        cout << stu << endl;
        
        return 0;
    }
    
  • 如果不希望返回引用被利用,可在其前面加const;

    #include<iostream> 
    using namespace std; 
    
    int& func1(int& n)
    {
    	return ++n;
    }
    const int& func2(int& n)
    {
    	return ++n;
    }
    
    int main()
    {
    	int a = 1;
        int& b = func1(a);
    	cout << func1(a) << "," << a << "," << b << endl;
    	func1(a) = 10;
    	cout << func2(a) << "," << a << "," << b << endl;
    	
    	const int& b2 = func2(a);
    	// func2(a) = 12; // error: assignment of read-only location ‘func2(a)’
    	cout << func2(a) << "," << a << "," << b << endl;
        
        return 0;
    }
    
类 class / 结构体 struct:

类/结构体中的每个变量都有自己独立的内存。

类/结构体数据对齐的问题:
  1. 遵循“缺省对齐”的原则。
  2. 32位CPU中,char可以占用任何地址、short可以占用偶数地址、int占用4的整数倍的地址、double占用8的整数倍的地址。
类/结构体内存布局:
  1. 32位CPU是以4字节为一个单位的,故内存布局在默认情况下,一般不是紧密排列的。
  2. 内存布局“遵循最大数”原则,即类中如果有double属性,则占用的总内存是8的倍数。
  3. 可以通过#pragma pack(n)中,通过设置不同的n,来表示内存排列的紧密程度;n=1,表示内存是紧密排列的。
结构体中的全部成员清零:
struct student 
{
    int age;
    char name[21];
}

// void* memset(void* dest, int ch, size_t count),只适用于结构体成员是c++的基本数据类型
student stu1;
memset(&stu, 0, sizeof(student));
student stu2;
memcpy(&stu2, &stu1);
复制结构体:

可以用=void memcpy(void* dest, void* src)函数。

结构体指针:
struct student
{
    char name[21];
    int age;
}

student stu;
student* stuPtr = &stu;
(*stuPtr).name;
stuPtr->name;
结构体数组:
struct student 
{
    char name[21];
    int age;
}

student stu[3];

stu[0].name;
(stu + i)->name;
(*(stu + i)).name = "ssuu";
结构体中的指针:
#include <iostream>
#include <cstring>
using namespace std;

struct PtrStruct
{
    int a;
    int* ptr;
};

int main()
{
    PtrStruct ptrStruct;
    // 会将结构体中的a=0,ptr=nullptr
    memset(&ptrStruct, 0, sizeof(PtrStruct));  

    ptrStruct.ptr = new int[20];
    // 如果结构体中的指针已经动态分配了内存空间,再用memset清零时,需要逐个字段清零
    ptrStruct.a = 0;  
    memset(ptrStruct.ptr, 0, 20 * sizeof(int));  
    
    return 0;
}
  1. 结构体中的指针指向的是动态分配的内存地址,对结构体直接用memset()函数可能会造成内存泄露,要逐个字段分情况的进行memset()清零。
  2. 类(class)只使用构造函数进行初始化,不要调用memset进行清零操作。
  3. 用memset清零时,会将结构中所有字节置0,如果结构体中有虚函数或结构体成员中有虚函数,则会将虚函数指针置0/置空,后续程序调用虚函数,空指针很可能导致程序崩溃!!!
联合体 union:

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

struct widget
{
    char brand[20];
    int type;
    union id
    {
        long id_num;
        char id_char[21];
    }id_val;
};

int main()
{
	widget prize; 
	cin >> prize.type;
	if (prize.type == 1) { 
		prize.id_val.id_num = 12;
		cout << prize.id_val.id_num << endl;
	} else {
		strncpy(prize.id_val.id_char, "hello", 6);
		cout << prize.id_val.id_char << endl;
	}
 
   return 0;
}
  • 共用体是一种数据格式,它能存储不同的数据类型,但只能同时存储其中的一种类型,即共用体只能存储int、long或double,而结构体可以同时存储int、long和double。
  • 联合体中的数据共享一块内存,且共同体占用内存的大小是它最大的成员占用的内存大小,且要满足内存对齐原则。
  • 联合体中的值为最后被赋值的那个成员的值。

匿名共用体:

struct widget
{
    char brand[20];
    int type;
    union     // 在定义时,创建匿名联合体变量,也可以嵌套在结构体中
              // 其成员位于相同地址的变量,故每次只有一个成员是当前的成员
    { 
        long id_num;
        char id_char[20];
    };
};

int main()
{
    widget prize; 
    if(prize.type == 1)
        cin >> prize.id_num;   
    else
        cin >> prize.id_char;
}
枚举 enum:

enum不仅能够创建符号常量,还能定义新的数据类型。

使用细节:

// 创建枚举类型wt,默认从0开始,还可以任意设置枚举量的值但必须是整数
enum wt{Monday, Tuesday, Wednesday, Thursday, Saturday, Sunday};

// 创建枚举变量,并赋初始值
wt weekday = Monday;
weekday = wt(1);           // 此时weekday = Tuesday
  1. 枚举值不可以做左值。
  2. 枚举变量可以赋值给非枚举变量,非枚举值不可以赋值给枚举变量。
符号常量 #define 或 const:

const常量:声明为常量的变量是只读的。

// 常量指针:不能通过解引用的方法修改内存中的值,但可尝试使用原始的变量修改。
const 数据类型* 变量名;
			
// 指针常量(引用):指向的变量不可改变,但可通过解引用修改变量在内存中的值;定义时必须初始化,否则没有意义。
数据类型* const 变量名;
			
// 常指针常量(常引用):指向的变量不可改变,也不能通过解引用修改变量在内存中的值。
const 数据类型* const 变量名;	

常量表达式constexpr:c++11引入,在编译时就会求值,提高了系统的性能。

c++11新增的long long类型:
  • VS中,long类型占4 bytes,long long类型占8 bytes。
  • linux中,long类型和long long类型,都占用了8 bytes。
自动推导类型:

在这里插入图片描述

c++11中,编译器在编译期时,推导auto声明的变量的数据类型,故不会造成程序运行效率的下降。

注意:

  • auto声明的变量,必须在定义时初始化;
  • 初始化的右值,可以是具体数值,也可以是表达式和函数的返回值;
  • auto不能作为函数的形参类型;
  • auto不能直接声明数组;
  • auto不能定义类的非静态成员变量;

auto的真正用途:

  • 代替冗长复杂的变量声明;

  • 代替函数指针类型;

    #include <iostream>
    using namespace std;
    
    int func(int val1, int val2)
    {
    	return val1 + val2;	
    }
    int main()
    {
        /*
            数据类型的别名:typedef 类型名 新的类型名;
            如typedef unsigned int size_t,为了避免类型名太长,造成代码可读性下降。
        */
    	// typedef定义函数类型
    	typedef int(f)(int,int);
    	f* fPtr1 = &func;
    	cout <<	fPtr1(1,2) << endl;
    	
    	// typedef定义函数指针类型
    	typedef int(*fPtrType)(int,int);
    	fPtrType fPtr2 = func;
    	cout << fPtr2(1, 2) << endl;
    	
    	// 声明函数指针:
    	int(*fPtr3)(int,int);
    	fPtr3 = func;   // 定义函数指针;
    	cout << fPtr3(1, 2) << endl;
    	
    	// 通过右值,auto能自动推导出函数指针类型
    	auto fPtr4 = func;
    	cout << fPtr4(1, 2) << endl;
    	
    	return 0;
    }
    
  • 用于lambda表达式;

void关键字:

void表示无类型,主要有如下用途:

  1. 函数返回值用void,表示函数没有返回值

  2. 函数形参:

    void,表示函数不需要参数(或让参数列表是空);

    void*,表示接受任意数据类型的指针;(要将void*类型转换成其他类型,需要显式转换)

  3. 其他类型的指针 --> void*指针,不需要转换;void*指针 --> 其它类型的指针,需要转换。

零初始化:
  • 零初始化值:int ==> 0指针 ==> nullptrbool ==> false
  • 三种零初始化方式:int a = {}int a = int()int a{}

类型转换:

c的类型转化:
  • 隐式类型转换:如double f = 1.0 / 3;
  • 显式类型转换:(类型说明符)(表达式);

存在的问题:任何类型之间都能进行转换,且编译器无法判断其正确性

c++的类型转换:
自动/隐式类型转换:

系统自动进行,不需要开发人员介入。

强制类型转换:

强制类型转换名<type> (express)

static_cast(最常用):

静态类型转换:编译的时候就会进行类型转换检查。不会产生动态类型转换的类型安全检查的开销。与c语言中的强制类型转换,差不多。

用途:

  1. 相关类型转换,比如整型和实型转换;

    int i = 5;
    double d = static_cast<double>(i);
    
    double d = 5.0;
    int i = static_cast<int>(d);
    
  2. 类中子类与父类之间的转换,且只能是子类转换为父类;

    class A
    {
        . . . . 
    }
    class B : public A
    {
        . . . .
    }
    
    B b;
    // 子类能转换为父类
    A a = static_cast<A>(b);
    
  3. void*与其他类型的指针之间的转换;

    int a = 10;
    void* ptr_int = &a;
    double* ptr_double = static_cast<double*>(ptr_int);
    
    • void*无类型指针可以指向任何指针类型(即万能指针

    • 主要用于函数的形参中用void*,即“实参的类型指针->void*指针->函数中使用的类型指针”

      // 其他类型指针 -> void* -> 其他类型指针
      void func(void* ptr)
      {
          double* ptr_double = static_cast<double*>(ptr);
      }
      
      int main()
      {
          int a = 10;
          func(&a);
      }
      

注意:一般不能用于指针类型之间的转换,比如int *、float *、double *等

int a = 10;
double* ptr_double = static_cast<double*>(&a);
// 会报错,static_cast不支持不同类型指针之间的转换
reinterpret_cast:

重新解释,将操作数的内容解释为另一种不同的类型(可以处理无关类型的转换),且编译时就会进行类型转换检查。

  • 不检查指向的内容,也不检查指针类型本身。
  • 要求转换前后的类型所占用的内存大小一致,否则会引发编译时错误。
  • <目标类型>和(表达式)中必须有一个似乎指针/引用类型。
  • 不能丢掉(表达式)中的const和volitale属性。

常用与两种转换:

void func(void* ptr)
{
    long long i = reinterpret_cast<long long>(ptr);
    cout << i << endl;
}

int main()
{
    long long i = 10;
    // 要求转换前后,类型占用的字节数一致。这里,long long占用8字节、指针也占用8直接
    func(reinterpret_cast<void*>(i));
}
  • 将指针/引用转换成整型变量。
  • 将整型变量转换成指针/引用。
  • 改变指针/引用类型,不需要像static_cast要借助void*
dynamic_cast:
  1. 动态转换,主要用于运行时,类型识别和检查
  2. 只能用于含有虚函数的类,必须用在多态体系中,用于类层次间的向上和向下转换(向下转换时,如果是非法的指针则返回NULL)。
  3. 主要用于父类和子类之间的转换(父类指针指向子类对象,通过dynamic_cast把父类指针转换为子类指针)
const_cast:
#include <iostream>
#include <string>
#include <cstring>
using namespace std;

int main()
{
	string str1(5, ' ');
	string str2 = "123";
	
	strncpy(const_cast<char*>(str1.data()), str2.c_str(), str2.size()); 
	cout << str1 << ",";
	return 0;
}

只能去除指针 或者 引用的const属性。

const int a1 = 1;
// int a2 = const_cast<int>(a1);   //  报错,a1不是指针或者引用

const int* a2 = &a;
int* a4 = (int*)(a2);   // C风格的强制转换
int* a3 = const_cast<int*>(a2);   // c++风格的强制转换
总结:
  • c++推出的类型转换替换c风格的类型转换,采用更严格的语法检查,降低使用风险
  • 一般static_castreinterpret_cast,能够很好的取代C语言风格的类型转换。

静态变量:

  • 静态变量的存储方式和生命周期:属于静态存储方式,其存储空间为内存中的静态数据区;该区域的数据在整个程序的运行期间不会释放,所以其生命周期为整个程序运行时间段

  • 静态局部变量:定义在函数体内的变量。

    1)当对静态局部变量进行初始化时,只初始化一次,且必须是常量或常量表达式;

    2)局部静态变量,编译阶段不分配内存;只有在执行并且调用其所在的函数时,才会分配内存

  • 全局变量与静态全局变量:两者的区别是作用域不同。

    1)非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在所有源文件中都是有效的

    2)静态全局变量只在定义该变量的源文件内有效,可以增加安全性和避免不同源文件同变量名冲突问题。

静态、动态分配内存:

动态内存分配:

运行期间分配,程序结束前,必须释放内存分配的空间,否则会造成内存泄露。

程序执行较慢,因内存在程序执行时,才进行分配(一般分配的是连续的内存空间)。

指向动态分配的内存空间的指针,在使用完成后需要程序员释放掉,否则会造成内存泄漏。

动态内存分配的注意事项:

堆、栈的不同用途和区别:
  1. 栈:空间有限,编译器自动分配,速度较快
  2. 堆:只要不超过实际的物理内存,而且在操作系统能分配的最大内存大小内都可以分配分配速度慢;通过malloc/free、new/delete来实现。
实现:
C语言:

通过malloc/free从堆区申请和释放内存,malloc(memory allocation)动态内存分配。

void* malloc(int NumBytes)   // NumBytes:是要分配的字节数
// 分配成功,返回指向被分配内存的指针;分配失败,返回NULL

当不使用这段内存时,要用void free(*FirstBytes)函数,将这段内存释放并被系统回收,需要时重新分配。

char* ptr = (char*)malloc(13 * sizeof(char));   // 在堆中分配四个字节

if (otr != nullptr)
{
    strcpy_s(ptr, 13, "hello world!");
    cout << ptr << endl;
    
    // 释放内存
    ptr = nullptr;
    free(ptr);
}

给申请的100个整型内存空间赋值:

// 给申请的100个整型内存空间赋值0-100
// 分配400个字节
int* ptr = (int*)malloc(100 * sizeof(int));
if (ptr != nullptr)
{
    // 通过指针ptr1,给指向ptr的内存空间赋值
    int* ptr1 = ptr;
    for (int i = 0; i < 100; i++)
    {
        *ptr1++ = int(i);      // 等价于*(ptr1 + i) = i;
    }

    // 输出申请的100个整型内存空间的值
    if (ptr1 != nullptr)
    {
        for (int i = 0; i < 100; i++)
        {
            cout << *(ptr + i) << " ";      
        }
        cout << endl;
    }

    // 释放申请的内存  
    free(ptr); 
    ptr = nullptr;
}
c++语言:

new/delete(运算符(标识符))分配和释放在堆区的内存。

// new使用的一般格式:
// 1. 申请一个堆区的内存:
指针变量名 = new 类型标识符;
指针变量名 = new 类型标识符(初始值);
// 2. 申请一些连续的堆区的内存:
指针类型名 = new 类型标识符[内存单元的个数];

new:动态的分配内存,然后调用对应的构造函数(递归调用各个成员变量的构造函数)(编译器自动进行)。new类对象时,加不加括号的差别:

A* a1 = new A();     // 加圆括号
A* a2 = new A;       // 不加圆括号
  1. 如果new一个空类,则两种方式并无区别。
  2. 如果类中含有成员变量,则带括号的初始化会将一些成员变量相关的内存清零,但并非所有内存空间全部清零(虚函数表指针不能清零)
  3. 当类中有构造函数时,带/不带圆括号完全相同。

delete:调用对应的析构函数(编译器自动进行),然后释放内存。

在这里插入图片描述

注意:两者void* operator new(size_t size)void* operator new[](size_t size)内部函数体相同,只是编译器推导出的size(字节数)不同

nullptr:

c++11引入的新关键字nullptr,代表空指针;NULL:也代表空指针,实际上是整型数0。

  • 引入nullptr,能够避免整数和指针之间发生混淆。
  • NULL和nullptr实际上是不同的类型,之后涉及指针时就用nullptr。
动态分配内存的布局:

除了需要的内存外,为了管理动态分配的内存故还需要一些额外信息(频繁的动态分配会造成资源的极大浪费,特别是申请小块内存时)。
在这里插入图片描述

malloc与free的实现原理:
  1. malloc底层是brkmmap系统调用实现的,free底层是unmap系统调用实现的。

  2. malloc小于128k的内存,使用brk分配内存(将数据段(.data)的最高地址指针_edata往高地址推);

    malloc大于128k的内存,使用mmap分配内存,在堆和栈之间(称为文件映射区域的地方),找一块空闲内存分配;

    这两种方式分配的都是虚拟内存,没有分配物理内存。当第一次访问已分配的虚拟地址空间时,会发生缺页中断,操作系统负责分配物理内存,并建立虚拟内存和物理内存间的映射关系。

  3. 操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表并寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表中删除并分配给程序。

被free回收的内存是立即返归还操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。

同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

静态内存分配:

  • 编译阶段分配,并在程序结束时自动归还给系统。
  • 较快,因程序在编译阶段即已决定内存所需要的容量,但这容易造成内存的浪费。

内存泄露的问题:

一般程序中已动态分配的堆内存,由于某种原因程序未能及时释放或者无法释放,造成系统资源的浪费,导致程序运行速度减慢甚至系统崩溃。

内存泄漏在服务器上尤为明显,因服务器上的程序一旦运行不能随意中断,故不断泄露内存会使系统资源被极大的占用和浪费,导致出现程序运行速度减慢或者崩溃的问题。

数据的输入输出控制:

使用控制字符:

#include <iostream> 

cout << setprecision(3) << ...              // 保留三位有效数字
cout  << fixed << setprecision(3) << ...    // 保留小数点后三位有效数字
cout  << scientific << setprecision(3) << ...  // 小数点后三位有效数字的科学计数
cout  << setfill('%') << setw(5) << num1 << nnum2 << endl;

cout的输出顺序(自左向右),计算顺序(自右向左):

  • cout作为输出流,先将数据从右向左读入缓冲区,再从缓冲区读写到屏幕(类似堆栈)。

  • cout本质上是类ostream的一个对象。

    
    备注
    一般形式:
    void *malloc(int NumBytes)
    // NumBytes:是要分配的字节数
    // 分配成功,返回指向被分配内存的指针;分配失败,返回NULL
    #include<iostream>
    #include<cstdio>
    
    // 宏定义namespace x {}:
    #define BEGINS(x) namespace x {
    #define ENDS(x) } 
    
    // 命名空间SelfCout:
    BEGINS(SelfCout)
    	
    class ostream 
    {
    public:
        // 返回值为ostream&,可使“<<运算符”连续使用
        ostream& operator<<(int x);
        ostream& operator<<(const char *x);
    };
    
    ostream& ostream::operator<<(int x) {
        printf("%d", x);
        return *this;
    }
    
    ostream& ostream::operator<<(const char *x) {
        printf("%s", x);
        return *this;
    }
    ostream cout;  // cout是类ostream的一个对象
    
    ENDS(SelfCout)
    
    int main()
    {
        int n = 123, m = 456;
        std::cout << n << " " << m; std::cout << std::endl;
        SelfCout::cout << n << " " << m; std::cout << std::endl;
    	
        return 0;
    }
    

c++的关键字:

在这里插入图片描述

c++的运算符:

在这里插入图片描述

按运算性质:算数运算符、自增自减、赋值运算符(/=、%=)、关系运算符、逻辑运算符(&&、||、!)、位运算符(右移操作比较复杂(逻辑右移、算数右移:左边空缺位的填充不同)、左移操作(直接给右边的空缺位补0))、杂项运算符。

按运算对象:单目运算符(一个运算对象)、双目运算符(两个类型相同的运算对象)、三目运算符(条件运算符 condition ? X : Y)。

其他运算符:

  1. 字节数运算符sizeof ,返回变量的大小;
  2. 指针运算符&var ,返回变量的地址;
  3. 指针运算符*var ,返回变量var;

结构体、类:

结构变量、对象:一块能够存储数据,且具有某种类型的内存空间。

  • c中,定义一个属于该结构的变量,称为结构变量。
  • c++中,定义一个属于该类的变量,称为对象。

c++中,结构体和类具有相似性,区别主要有两点:

  1. 内部的成员变量、成员函数,默认的访问权限不同:结构体 – public、类 – private。
  2. 继承,默认的权限不同:结构体 – public、类 – private。

结构体:

// 使用结构体作为函数的形参 
struct Student
{
    int num;
    char name;
} student;

void func1(Student tempStu)   // 结构体作为函数的形参
{
    tempStu.num = 20;
    strcpy_s(tempStu.name, sizeof(tempStu.name), "lisi");
}
// 效率低,因为在实参传递给形参时,发生了内存内容的拷贝操作 
func1(student);

void func2(Student& tempStu)  // 函数的形参变为结构体引用
{
    tempStu.num = 20;
    strcpy_s(tempStu.name, sizeof(tempStu.name), "lisi");
}  
func2(student);

void func3(Student* tempStu)  // 指向结构体的指针做函数参数
{
    tempStu->num = 20;
    strcpy_s(tempStu->name, sizeof(tempStu->name), "lisi");
}   
func3(&student);

静态对象与全局对象的构造顺序:

函数/类中的静态对象:

  • 多次调用函数,静态对象只会创建一次,即一个函数的静态局部变量在函数被多次调用时只初始化一次。

    #include <iostream>
    using namespace std;
    
    void func()
    {
    	static int a = 1;   // 多次调用函数,静态对象只会创建一次
    	++a;
    	cout << a << " ";
    }
    
    int main()
    {
    	func(); func(); func(); // 2 3 4
    	return 0;
    }
    
  • 类中的静态对象只有声明且定义后,才能被调用。

全局对象的构造顺序:

  • 如果项目中,有多个.cpp文件且每个源文件中都定义了不同的全局对象,则这些全局对象的构造顺序是无规律的
  • 不能在构造某个全局对象时,直接使用另一个全局对象(无法确定该对象是否在使用前被构造);

临时对象:

产生临时对象的情况和解决:

  1. 以传值的方式给函数传递参数;

  2. 类型转换生成的临时对象;

    类名 obj;
    obj = 100;   
     // 这里产生了一个真正的临时变量,后干了三件事:
    // 1)用100创建一个该类的临时对象
    // 2)调用拷贝赋值运算符把这个临时对象里的各个成员赋值给obj对象
    // 3)调用析构函数,销毁创建的临时对象
    
    // 把定义对象和给对象赋值放在同一行:
    // 这就为obj对象预留了空间,避免了使用临时对象
    类名 obj =100;
    
  3. 隐式类型转换以保证函数调用成功;

  4. 函数返回局部对象时;

注意:c++中,只会为const引用(const string& str)产生临时对象;不会为非const引用(string& str)产生临时对象。

深、浅拷贝的问题:

浅拷贝:只拷贝指针地址

在这里插入图片描述

  • c++默认为每个类生成的“拷贝构造函数”与“重载的赋值运算符”,都是浅拷贝。
  • 优点:节省空间;缺点:容易引发多次释放、内存泄漏的问题。

深拷贝:重新分配内存,拷贝指针指向的内容

  • 缺点:浪费空间;优点:不会导致多次释放;
#include <iostream>
#include <cstring>
using namespace std;

class Stu
{
public:
	int age;
	string name; 
	char* phone;   // 使用堆区开辟的内存空间
public:
	Stu() : age(0), name(""), phone(nullptr)
	{
		cout << "default constructor" << endl;
	}
	Stu(int m_age, string m_name, char* m_phone) : age(m_age), name(m_name)
	{
		this->phone = new char[sizeof(m_phone)];
		memcpy(this->phone, m_phone, sizeof(m_phone));
		cout << "with the constructor" << endl;
	}
	Stu(const Stu& stu)
	{
		this->age = stu.age;
		this->name = stu.name;
		/*
		// 此时,会存在“浅拷贝”的问题,如果Stu stu3(stu2)中stu2先释放,stu3.phone的使用就会崩溃
		this->phone = stu.phone;  
		*/
		// 深拷贝:
		this->phone = new char[sizeof(stu.phone)];
		memcpy(this->phone, stu.phone, sizeof(stu.phone));
		cout << "copy constructor" << endl;
	}
	~Stu()
	{
		delete[] this->phone;
		this->phone = nullptr;
		cout << "destructor" << endl;
	} 
	friend ostream& operator<<(ostream& out, const Stu& stu);
};

ostream& operator<<(ostream& out, const Stu& stu)
{
	out << stu.age << "," << stu.name << "," << stu.phone << endl;
	return out;
}

int main()
{
	Stu stu1;
	Stu* stu2 = new Stu(12, "wowo", (char*)"121212");
	Stu stu3(*stu2);
	delete stu2;  // 如果Stu类的拷贝函数中存在“浅拷贝”的问题,则stu2先释放,stu3.phone的使用就会崩溃
	cout << stu3 << endl;
	
	return 0;
}

如何兼顾两者的优点:

  1. 引用计数:会带来额外的内存开销;

  2. c++新标准中,std::move()移动语义;

    const int len = 100;
    
    class String 
    {
    public:
        // 普通构造函数
        String(const char* str = NULL)
        {
            if (str == NULL)
            {
                this->data = new char[1]
                this->data = '\0';
            }
            else 
            {
                int len = strlen(str.data);
                // 字符串结束符'\0'占一字节
                this->data = new char[len + 1]; 
                strcpy(this->data, str);
            }
        }
    
        // 拷贝构造函数
        String(const String& str)
        {
            int len = strlen(str.data);
            // 字符串结束符'\0'占一字节
            this->data = new char[len + 1]; 
            
            if (this->data != NULL)
            {
                strcpy(this->data, str);
            }
            else 
            {
                 exit(-1);
            }
        }
    
        // 赋值运算符
        String& operator=(const String& str)
        {
            if (this->data != &str)
            {
                delete[] this->data;
                this->data = new char[strlen(str.data)+1];
                if (!this->data)
                {
                    strcpy(this->data, str.data);
                }
           }
            return *this;
        }
    
        // 移动构造函数
        String(String&& str)
        {
            if (str.data != NULL)
            {
                // 资源的让渡
                this->data = str.data;
                str.data = NULL;
            }
        }
    
        // 移动赋值运算符
         String& operator=(String&& str)
         {
              if (this->data != NULL)
              {
                  delete[] this->data;
    
                  // 资源的让渡
                  this->data = str.data;
                  str.data = NULL;
             }
              return *this;
          }
    
            virtual ~String()
            {
                if (this->data != NULL)
                {
                    delete[] this->data;
                    this->data = nullptr;
                }
            }
    public:
        char* data;
    }
    
    int main()
    {
        String str1("hello");
        String str2(std::move(str1));
        String str3 = std::move(str2);
    }
    

    左值/右值、左值引用/右值引用、万能引用、move、移动语义、完美转发:

    template<class T>
    void swap(T& a, T& b)
    { 
        // 以下三个语句在执行时,都会发生拷贝动作
        const T tmp = a;
        a = b;
        b = tmp;
    }
    
    template<class T>
    void swap(T& a, T& b)
    { 
        // "perfect swap"
        T tmp = std::move(a);
        a = std::move(b);
        b = std::move(tmp);
    }
    

    左值、右值 :

    左、右值区别:
    • 左值代表一个地址;右值代表一个值;(c++中,一个表达式只能是左值或者右值之一)
    • 左值是可以被引用的数据,可以通过地址访问,如变量、数组元素、结构体成员、引用和解引用的指针;
    • 左值,可以同时具有左值和右值属性;如 i = i + 1;
    • 右值:非左值,包括字面常量(用双引用包含的字符串除外,它是有地址的)和包含多项的表达式;
    class A;
    
    int i = 3;  // i是左值,3是右值
    i = i + 3;   // 左边的i是左值,右边的i+3是右值
    
    A func()
    {
        return a;
    }
    A a1 = func1();   // a1是左值,func1()返回的返回值类型是A故为右值
    
    A& func2(A& a)
    {
        return a;
    }
    A a2 = func2();   // a2是左值,func2()返回的返回值类型是A&故为左值
    
    /*
    总的来说:
    1. 右值无法取地址,而左值可以;
    2. 左值有名字,而右值没有;
    3. 表达式结束后,左值仍然存在,右值就不再存在;
    */
    
    c++11中扩展了右值的概念,分为:纯右值、将亡值

    纯右值:

    1. 非引用返回的临时变量;
    2. 运算表达式产生的结果;
    3. 字面常量(c语言风格的字符串,是有地址的);

    将亡值:与右值引用相关的表达式

    1. 将要被移动的对象;
    2. T&&函数的返回值;
    3. std::move()函数的返回值;
    4. 转换成T&&类型的转换函数的返回值;
    使用左值的运算符:
    1. 赋值运算符=:整个赋值语句的结果仍然是左值;
    2. 取址符&;
    3. string、vector容器:
      • 通过判断运算符能够对数字进行直接操作,进而可以判断是否是左值(不能直接对数字进行操作,则该运算符要用左值);
      • 下标[ ]就是一个左值;
      • 迭代器iter也是左值,即vector<int>::iterator iter
    不是左值就是右值:

    临时变量被当作右值。

    引用类型:c++98中均为左值引用,c++11开始出现右值引用:

    左值引用lvalue reference(绑定到左值),即给左值起别名:
    int val1 = 3;
    // 左值引用:
    int& val2 = val1;
    
    • 没有空引用的说法:左值引用初始化时,必须绑定到左值;

    • 引用左值时,必须绑定到左值上(不能绑定到右值(数字)上);

    • 常量左值引用,是一个万能的引用,可以绑定非常/常量左值、右值,缺点:只读不能修改

      int b = 10;
      const int c = 10;
      
      const int& rb = b;   // 常量左值引用,绑定非常量左值
      const int& rc = c;    // 常量左值引用,绑定常量左值
      
      const int& rval = 10;  // 常量左值引用,绑定右值
      
    右值引用rvalue reference(绑定到右值),即给右值起别名:
    // 右值引用:
    const int&& val = 4;
    // 系统利用的是临时变量temp
    // int tempVal = 4;
    // const int& val = tempVal;
    
    int&& val2 = val + 3;    // val+3是右值
    /* 右值有了名字,就变成了左值 */
    
    • &&,系统希望用右值引用来绑定一些即将被销毁或者临时的对象上。

      class A;
      
      // 函数的返回值是右值(临时变量)
      A getTmp()
      {
          return A();
      }
      
      int main()
      {
          // 右值引用函数返回的临时变量
          A&& a = getTmp();
          // 这样在构造a的过程中,只调用了默认/有参构造函数,而没有调用拷贝构造函数,效率更高
      }
      
    • 右值引用的目的:c++11引入右值引用代表一种新的数据类型,来提高系统效率(把拷贝对象变成移动对象)。&&,常被用于移动语义中,即移动构造函数和移动赋值运算符的形参列表中。

    总结:
    1. 左值引用,使用T&,只能绑定左值;
    2. 右值引用,使用T&&,只能绑定右值;
    3. 已命名的右值引用,是左值
    4. 常量左值const T&,既可以绑定左值又可以绑定右值

    move函数(c++11标准库中的新函数):

    作用:将一个左值强制转换为右值。

    int val1 = 3;
    int && val2 = std::move(val1);
    // val2相当于val1的引用
    
    
    string str1 = "I love China!";
    string str2 = std::move(str1);
    // 调用string中的移动赋值运算符,将str1中的内容移动到str2中去了
    

    本质:将对象的状态/所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁/内存拷贝,所以可以提高利用效率、改善性能。

    #include <iostream>
    #include <vector>
    #include <string>
    using namespace std;
    int main()
    {
        string str = "Hello";
        vector<string> vctor;
        
    	// 调用常规的拷贝构造函数,新建字符数组,拷贝数据
        vctor.push_back(str);
        cout << str << endl;
        
    	// 调用移动构造函数,掏空str(掏空后尽量不要再使用str);该过程中,没有发生内存的拷贝和释放,只是所有权发生了变化
        vctor.push_back(std::move(str));
        cout << str << "\t" << v[0] << "\t" << v[1] << endl;
    }
    

    万能引用T&&(存在的前提为模板参数类型)、const T&:

    1. 如果模板(类模板、函数模板)中,参数为T&&,那么既可以接受左值引用又可以接受右值引用。

      #include <iostream>  
      using namespace std; 
      
      template<typename T>
      void funcLRVal_(T&& val)   // T&&作为参数类型,既可以接受左值,又可以接受右值
      {
          ...
      }
       
      int main()
      {
             int a = 10; 
      	funcLRVal(a);
      	funcLRVal(10);  
      
      	return 0;
      }  
      
      • const修饰后,即const T&&,就只能接受右值引用。

        #include <iostream>  
        using namespace std; 
        
        template<typename T>
        void test(const T&& val)
        {
        	cout << "void test(const T& val)" << endl;
        }
        
        int main()
        {
               int a = 10; 
        	test(10);
        	// test(a);   // 报错
        	return 0;
        }  
        
      • int&&vector<T>&&,“具体类型”或“非T&&”,则均不是万能引用。

      • 类模板的成员函数,在类实例化后,成员函数的参数类型已确定,即并不再是模板参数,故不会是万能引用(除非成员函数是函数模板,且参数类型为T&&)。

    2. const T&,既能接受左值引用,又能接受右值引用。

      #include <iostream>  
      using namespace std; 
      
      template<typename T>
      void test(const T& val)
      {
      	cout << "void test(const T& val)" << endl;
      }
      
      int main()
      {
             int a = 10; 
      	test(10);
      	test(a);
      	return 0;
      }
      

      缺点:函数体内不能对参数进行修改。

    移动语义:

    #include <iostream> 
    #include <cstring>
    using namespace std;
    
    class A
    {
    public:
    	int* m_data = nullptr;  // 指向堆区资源的指针,类内初始化
    	A() = default;    // 启用默认的构造函数
    	void alloc()
    	{
    		m_data = new int;        // 分配堆区内存
    		memset(m_data, 0, sizeof(int));   // 将分配的内存初始化为0
    	}
    	A(const A& a)   // 拷贝构造函数
    	{
    		cout << "A(cosnt A& a)" << endl;
    		if (m_data == nullptr) { alloc(); }
    		memcpy(m_data, a.m_data, sizeof(int));
    	}
    	A& operator=(const A& a)  // 拷贝赋值函数
    	{
    		cout << "A& operator=(const A& a)" << endl;
    		if (this == &a) { return *this; }  // 避免"自我赋值"
    		if (m_data == nullptr) { alloc(); }
    		memcpy(m_data, a.m_data, sizeof(int));
    		return *this;
    	}
    	~A()
    	{
    		delete m_data;
    		cout << "~A()" << endl;
    	}
    	
    	A(A&& a)   // 移动构造函数,形参不能用const修饰,因最后要将a.m_data置空
    	{
    		cout << "A(const A&& a)" << endl;
    		if (m_data != nullptr)  // 如果已分配内存,则先释放掉
    		{ 
    			delete m_data;  
    		}
    		m_data = a.m_data;   // 将源对象中的指针指向的内存地址,赋值给新对象中的指针
    		a.m_data = nullptr;  // 将源对象中的指针置空
    	}
    A& operator=(A&& a)
    {
    	cout << "A&& A(A&& a)" << endl;
    	if (this == &a)        // 避免“自我赋值”
    	{  
    		return *this;
    	}
    	if (m_data != nullptr)  // 如果已分配内存,则先释放掉
    	{ 
    		delete m_data;  
    	}
    	m_data = a.m_data;   // 将源对象中的指针指向的内存地址,赋值给新对象中的指针
    	a.m_data = nullptr;  // 将源对象中的指针置空
    	return *this;
    }
    		
    };
    
    int main()
    {
        A a1;
    	a1.alloc();
    	*(a1.m_data) = 3;
    	cout << *(a1.m_data) << endl;
    	
    	A a2 = a1;  // 调用拷贝构造函数
    	cout << *(a2.m_data) << endl;
    	
    	A a3;
    	a3 = a2;   // 调用拷贝赋值函数
    	cout << *(a3.m_data) << endl;
    	
    	cout << ".............." << endl;
    	A a4(std::move(a1));   // 调用移动构造函数
    	A a5 = std::move(a2);  // 调用移动构造函数
    	A a6; a6 = std::move(a3);  // 调用移动赋值函数
        
        return 0;
    }  
    
    • 如果一个函数中有堆区资源,则需要编写拷贝构造函数和赋值函数,实现深拷贝。

    • 移动语义,通过直接使用源对象拥有的资源,可以节省资源申请和释放的时间。

      c++中所有容器,都实现了移动语义,避免对含有(堆区)资源的对象发生不必要的拷贝

    • 移动语义对于拥有资源(如堆区内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

    • 实现移动语义要增加两个成员函数:移动构造函数类名(类名&& 源对象)和移动赋值函数类名& operator=(类名&& 源对象)

      注意:形参不能用const修饰,因函数体内要源对象指向的内存进行置空。

    • c++提供std::move()方法将左值转义为右值,从而能方便使用移动语义。

      左值对象被转移资源后,不会立刻析构,只能在离开自己作用域的时候才能析构,如果继续使用左值中的资源,可能会发生意想不到的错误。

    完美转发:

    • 函数模板中,可以将参数 “完美转发” 给其内部调用的其它函数。

      “完美” 指的是:①准确地转发参数的值,②保证被转发参数的左、右值属性不变

      完美转发与否,影响参数在传递过程中,采用拷贝语义还是移动语义。

    • 为实现完美转发,c++11提供的方案:

      #include <iostream> 
      #include <cstring>
      using namespace std;
      
      void func(int&& val)
      {
      	cout << "params are right value" << endl;
      }
      
      void func(int& val)
      {
      	cout << "params are left value" << endl;
      }
      
      template<typename T>
      void funcLRVal(T&& val)   // T&&作为参数类型,既可以接受左值,又可以接受右值
      {
          func(val);
      }
      
      // 完美转发:
      template<typename T>
      void funcLVal(T& val)
      {
          func(val);
      }
      template<typename T>
      void funcRVal(T&& val)
      {
          func(std::move(val));
      }
      template<typename T>
      void funcLRVal_(T&& val)   // T&&作为参数类型,既可以接受左值,又可以接受右值
      {
          func(std::forward<T>(val));   // 将左值转发后仍是左值引用,右值转发后仍是右值引用
      }
      
      int main()
      {
          int a = 10;
      	
      	// 在模板函数中,模板函数的参数转发给func()函数后,都变成了左值
      	funcLRVal(10);
      	funcLRVal(a);
      	cout << endl;
      	
      	/* 实现完美转发的两种方案: */
      	// 1、通过两个模板函数,分别实现右值和左值的转发
      	funcLVal(a);
      	funcRVal(10);
      	// 2、采用forward<T>转换
      	funcLRVal_(a); 
      	funcLRVal_(10); 
      	
      	return 0;
      }  
      

      1)如果模板(类模板、函数模板)参数写为万能引用T&&,那么既可以接受左值引用又可以接受右值引用。

      #include <iostream>  
      using namespace std; 
      
      template<typename T>
      void funcLRVal_(T&& val)   // T&&作为参数类型,既可以接受左值,又可以接受右值
      {
          ...
      }
       
      int main()
      {
          int a = 10; 
          funcLRVal(a);
          funcLRVal(10);  
      
      	return 0;
      }  
      

      2)提供了模板函数std::forward<T>(参数),用于转发参数,

      template<typename T>
      void funcLRVal_(T&& val)   // T&&作为参数类型,既可以接受左值,又可以接受右值
      {
          func(std::forward<T>(val));   // 将左值转发后仍是左值引用,右值转发后仍是右值引用
      }
      
      • 如果参数是一个右值,转发后仍是右值引用;
      • 如果参数是一个左值,转发后仍是左值引用;
    • forward<T>通过T来决定,来推断并转发的。

      #include <iostream>
      using namespace std;
      
      void Print(int& val)
      {
      	cout << "Print(int& val)" << endl;	
      }
      
      void Print(int&& val)
      {
      	cout << "Print(int&& val)" << endl;	
      }
      
      template <typename T>
      void func(T&& tmp)
      {
      	Print(std::forward<T>(tmp));	
      }
      
      int main()
      {
      	func(10);   // T :int、tmp :int&&
      	// 等价于:
      	Print(std::forward<int>(10));
      	
      	int i = 10;
      	func(i);    // T :int&、tmp :int& 
      	// 等价于:
      	Print(std::forward<int&>(i));
      	
      	return 0;
      }
      
    • 普通函数,实现完美转发。

      #include <iostream>
      using namespace std;
      
      void func(int& val) { cout << "void func(int& val)" << endl; }
      void func(int&& val) { cout << "void func(int&& val)" << endl; }
      
      void funcLR(auto&& tmpVal)
      {
      	func(std::forward<decltype(tmpVal)>(tmpVal)); 
      }
      
      int main()
      {
      	int i = 10;
      	funcLR(i);
      	funcLR(std::move(i));
      	
      	return 0;
      }
      
    • 构造函数模板中,使用完美转发,以及对拷贝/移动赋值的影响。

      #include <iostream>
      #include <string>
      using namespace std;
      
      class Human
      {
      public:
      	/* Human的构造函数: */
      	/*
      	// 初始化列表中,会调用string(const string& str)的拷贝构造函数
      	Human(const string& name) : _name(name) 
      	{ 
      		cout << "Human(const string& name)" << endl;
      	}
      	
      	// 右值传入后,name会变成左值;std::move()只会将左值转换为右值;
      	// 初始化列表中,会调用string(string&& str)的移动构造函数
      	Human(string&& name) : _name(std::move(name)) 
      	{
      		cout << "Human(string&& name)" << endl;
      	}
      	*/
      	
      	// 构造函数的完美转发:
      	// 法一:
      	//Human(auto&& name) : _name(std::forward<decltype(name)>(name))
      	//{
      	//	cout << "Human(auto&& name)" << endl;
      	//}
      	// 法二:
      	template<typename T>
      	Human(T&& name) : _name(std::forward<T>(name))
      	{
      		cout << "template<typename T> Human(T&& name)" << endl;
      	}
      	
      	/* Human的拷贝构造函数: */
      	Human(const Human& human) : _name(human._name)
      	{
      		cout << "Human(const Human& human)" << endl;		
      	}
      	
      	/* Human的移动构造函数: */
      	Human(Human&& human) : _name(std::move(human._name))
      	{
      		cout << "Human(Human&& human)" << endl;		
      	}
      	
      private:
      	string _name; 
      };
      
      int main()
      {
      	/* 构造函数: */
      	Human human1(string("hi"));
      	string name = "hi";
      	Human human2(name);
      	 
      	/* 拷贝构造函数: */
      	//Error:受到构造函数中的函数模板的影响,不能正常地调用到拷贝构造函数
      	//Human human3(human2);
      	//解决方案:通过std::enable_if解决
      	
      	const Human human4(string("hi"));
      	// 因human4 : const Human类型,故能正常地调用到拷贝构造函数
      	Human human5(human4);
      	
      	/* 移动构造函数: */
      	// 不受到构造函数中的函数模板的影响,能正常地调用到移动构造函数
      	Human human6(string("hi"));
      	Human human7(std::move(human6));
      
      	return 0;
      }
      // std::move()实现的移动构造,不受影响,可正常调用。
      // 只有const Human类型,才能正常地调用到拷贝构造函数;不加const,则会因构造函数模板的存在使程序报错。
      
    • 可变参数模板中,使用完美转发。

      #include <iostream>
      using namespace std;
      
      int func(int val1, int& val2)
      {
      	++val2;
      	return val1 + val2;
      }
      
      template <typename F, typename... T>
      //auto Func(F f, T&&... t) -> decltype(f(std::forward<T>(t)...))  // 存在丢失引用的可能
      decltype(auto) Func(F f, T&&... t)   // 解决上面提到的“引用丢失”的问题
      {
      	return f(std::forward<T>(t)...);
      }
      
      int main()
      {
      	int j = 10;
      	cout << Func(func, 20, j) << endl;
      	cout << j << endl;
      	
      	return 0;	
      }
      

      1)支持任意数量、参数类型的完美转发;

      2)可变参数模板,需要返回值时,可使用decltype(auto)作为返回值类型;

函数新特性、函数重载、inline内联函数、函数中const的使用、递归函数:

函数新特性:

  • 函数定义中,形参如果在函数体中没有使用到,则可以不给形参变量名字,只给其类型。

  • 函数声明中,可以只有形参类型,没有形参名。

  • 函数定义:前置返回类型、后置返回类型。

    // 前置返回类型
    返回类型 函数名(形参)
    {
        . . . .
    }
    
    // 后置返回类型
    auto 函数名(形参) -> 返回类型
    {
        . . . .
    }
    

函数的使用细节:

  • 函数调用时,visual studio会从参数列表右边开始读变量的值,故函数定义时形参有默认值必须放在形参列表最后。

  • 函数传参时(如f(int x)f(int& x)f(int* x)f(int x[])f(int&& x)),传值、传地址、传引用的原则:

    1. 不需要在函数中修改实参:

    1)如果实参很小,比如内置数据类型、小型结构体,则可按值传递;

    2)如果实参是数组,则使用 const指针,没有为数组建引用的说法;

    3)如果实参是较大的结构体,则使用 “const指针 或 const引用”;

    4)如果参数是类,则使用 const引用,传递类的标准方式就是 const引用

    1. 需要在函数中修改实参:

    1)如果实参是内置数据类型,则使用指针,即func(&a)的调用表示要在函数中修改a的值;

    2)如果实参是数组,只能用指针;

    3)如果实参是结构体/类,则使用指针/引用;

  • 函数返回指针和引用

    int* func()
    {
        int tempVal = 4;
        return &tempVal;
    }
    
    int& func()
    {
        int tempVal = 15;
        return tempVal;
    }
    

    1)c++中,更习惯引用类型的形参,来取代指针类型的形参(防止值拷贝,引起的效率降低)。

    2)c++中,允许函数同名,但形参列表的参数类型或数量应该有明显的区别,即函数重载(函数名字相同、但参数个数/参数类型不同)。

  • 函数在反汇编后,每次调用函数都需要进行入栈和出栈的操作,故效率较低;处理传参/返回值/栈帧的产生和销毁,会带来一定的开销;

    如果函数体较小,为了避免频繁的入栈和出栈,可以将调用函数 --> 直接嵌入一段代码,从而节省计算开销。

    1)c语言:采用宏定义一个函数:define Multi(x) (x)*(x-1)。由于x可能是一个表达式,故需要加(),避免出现错误。

    2)c++采用inline/constexpr关键字修饰函数:

    // 1、可以使用内联函数(函数定义前加关键字inline),“编译阶段”直接将代码内嵌,但编译器只是作为参考
    // 特点:
    // 1)如果函数是内联函数,则在编译时,编译器会把该函数的代码副本,放置在每个调用该函数的地方,即采用空间换时间的方式。
    // 2)体积小,频繁调用的函数,可通过引入内联函数inline,提高程序性能。 
    inline 前置返回类型 函数名(形参)
    {
        . . . .
    }  
    // 注意:内联函数的定义要放在头文件:这样在用到该内联函数的.cpp文件时,都能够通过#include头文件,找到这个内联函数的函数本体,并尝试将该函数的调用改为函数本体调用。
    // 优缺点:存在代码膨胀的问题,故内联函数体必须小(循环、递归、分支,尽量不要出现在函数体中)。
                        
    // 2、c++11引入的关键字constexpr(该关键字修饰的函数,可以看作更严格的内联函数),保证函数或对象的构造函数是编译时常量。
    constexpr int get_five() {return 5;}
    int some_value[get_five() + 7];  // Create an array of 12 integers. Valid C++11
    /*
    	c++11,constexpr函数必须满足下述限制:
    	1)函数返回值不能是void类型;
    	2)函数体不能声明变量或定义新的类型;
    	3)函数体只能包含编译期语句:声明、null语句或者一段return语句,不能是运行期语句;
    	5)在形参实参结合后,return语句中的表达式为常量表达式;
    	在编译时若能求出其值,则会把函数调用替换成结果值,故相比宏来说没有额外的开销。
    	所有被声明为constexpr的非静态成员函数也隐含声明为const(即函数不能修改*this的值,即this是常量指针)。
    	
    	c++14放松了这些限制,声明为constexpr的函数可以含有以下内容:
    	1)任何声明,除了:static/thread_local变量、没有初始化的变量声明;
    	2)条件分支语句if和switch;
    	3)所有的循环语句,包括基于范围的for循环;
    	4)表达式可以改变一个对象的值,只需该对象的生命期在声明为constexpr的函数内部开始。包括对有constexpr声明的任何非const非静态成员函数的调用。 
    */
    
    #include<iostream>
    using namespace std;
    
    // C++98/03
    template <int N>
    struct Factorial_Cpp03
    {
    	const static int value = N * Factorial_Cpp03<N - 1>::value;
    };
    // 递归的基准点
    template <>
    struct Factorial_Cpp03<0>
    {
    	const static int value = 1;
    };
    
    // C++11
    constexpr int factorial_Cpp11(int n)
    {
    	return n == 0 ? 1 : n * factorial_Cpp11(n - 1);
    }
    
    // C++14
    constexpr int factorial_Cpp14(int n)
    {
    	int result = 1;
    	for (int i = 1; i <= n; ++i)
    	{
    		result *= i;
    	}
    	return result;
    }
    
    int main()
    {
    	static_assert(Factorial_Cpp03<3>::value == 6, "error");
    	cout << Factorial_Cpp03<3>::value << endl;
    	static_assert(factorial_Cpp11(3) == 6, "error");
    	cout << factorial_Cpp11(3) << endl;
    	static_assert(factorial_Cpp14(3) == 6, "error");
    	cout << factorial_Cpp14(3) << endl;
    
    	return 0;
    }
    
    /*
    	const 和 constexpr 变量间的主要区别:
    	1)const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化;
    	2)所有 constexpr 变量均为常量,因此必须使用常量表达式初始化。
    */
    

函数重载:

  • 函数重载是指设计一系列同名不同参的函数,让他们完成相同/相似的工作。

    实际中,可以重载功能相同但参数类型不同的函数,但不要重载功能不同的函数,会降低代码的可读性。

  • c++允许同名函数,但条件是:形参个数、数据类型、排列顺序要不同,但const、返回值,不作为函数重载的特征。

  • 注意:

    1)重载函数时,如果数据类型不匹配,c++会尝试进行类型转换并与形参进行匹配,若转换后有多个函数能匹配上则会报错;

    2)引用可作为函数重载的条件;

    void func(string str, int i);
    void func(string& str, int i);
    
    // 调用void func(string& str, int i);
    func(a, 10);
    
    // 调用void func(string str, int i);
    func("wowo", 10);  
    

    3)c++名称修饰:编译时,会对每个函数名进行加密,替换成不同名的函数;

const用法:

  1. 函数形参中带 const:

    • c++更习惯引用类型的形参,来取代指针类型形参(防止值拷贝,引起的效率降低);但这可能导致修改形参值,使得实参值也被无意修改,形参中加入 const 可避免无意中对形参的修改导致实参被更改的问题

      struct Student
      {
          int num;
          char name;
      }
      void func(const Student &tempStu)
      {
          ...
      }
      
      Student student;
      func(student);
      
    • 加入const,可以使实参类型更灵活,既可以接受普通的数据类型,也可以接受常量的数据类型(包括常数)。

      // 使用结构体作为函数的形参 
      struct Student
      {
          int num;
          char name;
      }
      void func(const Student &tempStu)
      {
          tempStu.num = 20;
          strcpy_s(tempStu.name, sizeof(tempStu.name), "lisi");
      }
      
      Student stu1;
      func(stu1);        // 接受普通的数据类型
      const student &stu2 = stu1;
      func(stu2);       // 接受常量的数据类型(包括常数)
      
  2. 常量指针和指针常量的区别:

    const char* ptr 等价于 char const *ptr,ptr指向的东西,不能通过ptr修改

    char* const ptr,ptr一旦指向一个东西,之后就不能再指向其他东西;但可以修改ptr指向的目标内容

递归函数:

递归,是一种重要的编程思想,可以通过数学归纳法严格证明。

递归设计的基本准则:

  • 基准情况:无须递归就能解出
  • 不断推进:每一次递归调用,都必须使求解状况朝接近基准情形的方向推进
  • 设计准则:假设所有的递归调用都能运行
  • 合成效益法则:求解一个问题的同一个实例,切勿在不同的递归调用中做重复性的工作

缺点:导致时间(需要大量重复的运算)和空间(需要开辟大量的栈空间)的浪费

递归的优化:

以求取斐波那契数列为例,提出不同的优化策略。

在这里插入图片描述

  1. 尾递归:所有递归形式的调用都出现在函数的末尾;

    int f(int n, int ret0, int ret1)
    {
        if (n == 0)
        {
            return 0
        }
         if (n == 1)
         {
              return 1;
         }
         return f(n-1, ret1, ret0 + ret1) ;
    }
    
  2. 使用循环替代;

    int f(int n)
    {
        int n0 = 0;    int  n1 = 1;
        for (int i = 2; i < n; i++)
        {
            temp = n0;
            n0 = n1;
            n1 = temp + n0;
        }
        return n1;
    }
    
  3. 使用动态规划,即采用"空间换时间"的策略;

    int recursion_space[1000];
    
    int f(int n)
    {
        recursion[0] = 0;
        recursion[1] = 1;
        for (int  i = 2; i < n; i++)
        {
            if (recursion_space == 0)
            {
                recursion_space[i] = recursion_space[ i - 2] + recursion_space[i - 1];
            }
        }
        return recursion_space[n - 1];
    }
    

c++中的I/O流、I/O缓存区:

I/O流:

在这里插入图片描述

在这里插入图片描述

I/O缓存区:

在这里插入图片描述

标准的I/O,提供的三种类型的缓存模式:

  1. 按块缓存:如文件系统;
  2. 按行缓存:\n
  3. 不缓存;
#include<iostream>
using namespace std;

// cin、cout:采用的是按行缓存的方式
int main()
{
    int a;
     
    int count = 0;    
    while (cin >> a)   
    {
        cout << a << endl;
        count++;
        if (count == 5)
        {
            break;
        }
    }
    cin .ignore(numeric_limits<std::streamsize>::max(), '\n');       // 会删除掉缓冲区中,多余的脏数据

    char ch;
    cin >> ch;
    cout << ch << endl;
}

文件操作:

  1. 输入流的起点和输出流的终点,都可以是磁盘文件;是以块缓存进行读取的;

  2. 数据的持久化方式:文件和数据库;

  3. c++将每个文件,都看做是一个有序的字节序列,每个文件都以文件结束标志结束;

  4. 文件缓冲区,又称文件缓存,是系统预留的内存空间,由操作系统管理;

    在这里插入图片描述

    • 因磁盘的读写要比内存慢的多,通过缓冲区,可以极大降低磁盘的I/O次数,从而提高磁盘存取的速度;

    • 根据输出和输入流,分为输出缓冲区和输入缓冲区,且不同的流的缓冲区是相互独立的;

    • c++中,每打开一个文件,系统就会为它分配缓冲区,程序员只关心输出缓冲区即可。
      缺省模式下,输出缓冲区的数据满了,系统才将数据写入磁盘,极大的降低了磁盘的I/O次数,效率更高,但容易导致数据没有及时的写入磁盘(掉电可能遗失数据)。

      输出缓冲区的操作:

      flush()     // 刷新缓冲区,将缓冲区中的内容写入磁盘文件中;
      
      endl        // 换行,然后刷新缓冲区;\n'的功能:只有换行;
      
      unitbuf     // 设置fout输出流,在每次操作后自动刷新缓冲区;
      nounitbuf   // 设置fout输出流,让fout回到缺省模式下的缓冲方式;
      fout << unitbuf;
      fout << nounitbuf;
      
  5. 流的状态:eofbitbadbitfailbit。取值:1表示设置、0表示清除。

    eofbit    // 当输入流操作到达文件末尾时,将设置eofbit
    eof()     // 用于检查流是否设置了eofbit
    fin >> buffer;
    if (fin.eof() == true)
    {
        break;
    }
    cout << buffer << endl;
    
    badbit    // 无法诊断的失败破坏流时,将设置badbit(一般是系统错误,如存储空间不足)
    bad()     // 用于检查流是否设置了badbit
    
    failbit   // 当输入流操作未能读取预期的字符时,将设置failbit
    fail()    // 用于检查流是否设置了failbit
    

    当三个流的状态都是0时,表示一切顺利,good()成员函数返回true,否则返回false。

  6. 按照文件中数据的组织形式,可分为:

    • 文本文件:存放的是字符串,以行的方式组织数据;文件中的信息形式为ASCII码文件,每个字符占一个字节,方便阅读(解码),但占用的空间比较多。
    • 二进制文件:存放的不一定是字符串,以数据类型组织的数据,内容要作为一个整体来考虑,单个字节没有意义;文件中信息的形式与其在内存中的形式相同,由0、1组成,组织数据的格式与文件用途有关,但不方便阅读(解码)。
      为节省存储空间,还可采用压缩计数;为保证数据安全,也可采用加密技术。
  7. 文件的随机存取:

    文件位置指针:对文件进行读/写操作时,文件的位置指针指向当前文件读/写的位置;

    获取文件的位置指针:ofstream类的成员函数是tellp()ifstream类的成员函数是tellg()

    移动文件位置指针:ofstream类的成员函数是seekp()ifstream类的成员函数是seekg()

    std::istream& seekg(std::streampos _pos); 
    std::istream& seekp(std::streampos _pos);
    seekp(128); seekg(128);                 // 文件指针移动到128字节
    seekp(std::begin); seekp(std::end);     // 文件指针移动到开始或结尾 
    seekg(std::begin)seekg(std::end):
    
    std::istream& seekg(std::streamoff _off, std::ios::seekdir _Way);
    std::istream& seekp(std::streamoff _off, std::ios::seekdir _Way);
    
    // ios中定义的枚举类型:
    enum seek_dir {beg, cur, end};
    seekg(30, ios::beg);    // 从文件开始位置往后移动30字节
    seekg(-30, ios::cur);   // 从当前位置往前移动30字节;seekg(30, ios::cur):从当前位置往后移动30字节
    seekg(-30, ios::end);   // 从文件结束位置往前移动30字节
    

    对文件进行随机存储,如果文件中该处有内容,则会被覆盖掉原有的内容。

  8. 文件操作的步骤:

    1、打开文件用于读和写open,文件的打开方式:
        // 默认是以ASCII码的形式打开:
        ios::in打开文件进行读操作(ifstream默认模式)
        ios::out打开文件进行写操作(ofstream默认模式)
        ios::trunc如果文件存在,清除原文件的内容
        ios::app打开文件并在追加内容
        ios::ate打开一个已有输入或输出文件并查找到文件尾
        ios::nocreate如果文件不存在,则打开操作失败
        // 以二进制的形式打开:
        ios::binary以二进制方式打开
    2is_open()       // ifstream、ofstream是否为空,检查打开是否成功
    3、读或者写read、write
    4、检查是否读完EOF(end of file)
    5close()         // 关闭文件
    
    #include <iostream>
    using namespace std;
    
    int main()
    {
        string filename = "./test.txt";
        fstream fin(filename, ios::in);
        if (!fout)   // fout.is_open()
        {
            cout << "open the file is failure" << endl;
        }
    
        string buffer;  // 按行读文件,要保证缓冲区足够大
        // 写法一:
        while (getline(fin, buffer))
        {
            cout << buffer << endl;
        }
        // 写法二:
        while (fin >> buffer))
        {
            cout << buffer << endl;
        }
        fin.close();
        return 0;
    }
    
    const bufferLen = 1024;
    bool copyBinaryFile(const string& src, const string& dst)
    {
        ifstream in(src, ios::in | ios::binary);
        ofstream out(dst, ios::out | ios::binary | ios::trunc);
        if (!in || ! out)
        {
            return false;
        }
    
        // 从源文件读数据到缓冲区,从缓冲区写入文件中
        char temp[bufferLen];
        while(!in.eof())
        {
            in.read(temp, bufferLen);
            streamsize count = in.gcount(); // 从源文件读取到缓冲区的个数
            out.write(temp, count);    
        }
        in.close();
        out.close();
    
        return true;
    }
    
  9. 读写二进制文件:

    二进制文件以数据块的形式组织数据,把内存中的数据直接写入文件;

    二进制的文件格式多种多样,由业务需求而定:

    • MP3、MP4、bmp、jpg、png;
    • 自定义的二进制文件格式,只有程序员自己可知,即自定义的数据结构;

    写二进制文件:

    #include<iostream>
    #include <fstream>
    using namespace std;
    
    int main()
    {
        // 自定义后缀.dat,其中存放的是不同的Girl类对象
        fstream fout("./test.dat", ios::out | ios::binary);
        if (!fout.is_open())
        {
            cout << "open ths file is failure" << endl;
        }
    
        struct Girl
        {
            char name[31];
            int age;
            double weight;
        } girl;
        girl = {"lili", 12, 130.5};
        fout.write((const char*)&girl, sizeof(Girl));
        
        fout.close();
        return 0;
    }
    

    读二进制文件:

    #include<iostream>
    #include <fstream>
    using namespace std;
    
    int main()
    {
        // 自定义后缀.dat,其中存放的是不同的Girl类对象
        fstream fin("./test.dat", ios::in | ios::binary);
        if (!fin.is_open())
        {
            cout << "open ths file is failure" << endl;
        }
    
        struct Girl
        {
            char name[31];
            int age;
            double weight;
        } girl; 
        // 二进制文件以数据块的形式组织数据
        while (fin.read((const char*)&girl, sizeof(Girl)))
        {
            cout << girl.name << "," << girl.age << "," << girl.weight << endl;
        }
        
        fin.close();
        return 0;
    }
    
  10. 操作文本文件和二进制文件的更多细节:

    1)windows平台下,文本文件的换行标志是"\r\n";(以文本方式打开文件,写数据时系统会将"\n"转换成"\r\n",读数据时系统会将"\r\n"转换成"\n";以二进制方式打开文件,系统不会做任何转换)

    2)linux平台下,文本文件的换行标志是"\n";(以文本和二进制方式打开文件,系统不会做任何转换)

    3)读取文件时,

    • 以文本方式读取文件的时候,遇到换行符停止,读入的内容没有换行符
    • 以二进制方式读取文件时,遇到换行符不会停止,读入的内容中包含换行符(换行符被认为是数据);
  11. 实际开发中,从兼容性和语义的角度考虑:

    1)以文本模式打开文本文件,用的方法操作它;

    2)以二进制模式打开二进制文件,用数据块的方法操作它;
    3)不要以二进制模式打开文本文件,也不要用行的方法操作二进制文件,可能破坏二进制数据文件的格式(二进制的某个字节的取值可能是换行符,但也可能是整数中的某个字节);

std::move()和std::ref的对比:

std::ref()的作用。

  1. std::move():c++11引入的用于将左值转换为右值引用,故而可使编译器选择移动语义而非拷贝语义,从而优化性能。其允许在不复制对象的情况下,将资源从一个对象转移到另一个对象上,

    • 对象之间传递大型数据结构;
    • 临时对象的资源转移到持久化对象上

    注意:一旦std::move()进行资源的转移,这样源对象就不能再使用了。

  2. std::ref():c++11引入的用于创建引用包装器。当需要向函数传递引用时,尤其是使用标准库时如std::bind()、std::thread()等,避免了这些函数默认对参数的复制。

总的来说:

  • std::move()用于优化性能,通过将资源的从一个对象转移到另一个对象上,而不是复制资源。
  • std::ref()用来创建引用包装器,以便在需要传递引用而不是复制对象时使用。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白要努力sgy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值