首页 > 编程语言 >C++学习记录

C++学习记录

时间:2023-03-15 18:57:24浏览次数:49  
标签:const 函数 记录 int double C++ 学习 类型 指针

C++record notebook

基础导论

C++特性

具有c访问硬件的能力和面向对象程序的属性,以及更具有泛型编程的功能(使用模板进行编程)。

OOP(面向对象编程)

其中的方法有:自顶向下和自底向上编程思想。

compile编译,execute执行

源代码通过编译器编译目标文件,再通过连接程序启动代码库代码连接为可执行文件

预处理(在编译之前)

含有“#”的语句也叫宏定义。在预处理阶段需要将它替换掉。例如:

在源代码的头部#include部分,本质是:

将include的语句,在源代码被编译前,用它所对应的文件的代码替换此include语句,当然啦,实际并没有改动源文件,只是在编译前,预处理的文件,与该源代码文件组成复合文件,一起被发送给编译器进行编译成目标代码。在后续需要通过连接,才能组成可执行文件(.exe)被执行。

例如:

预处理前:
/* math.h*/
double add(double x, double y);

/* main.cpp */
#include"math.h"

int main(){
double x = 1.00,y = 2.00;
add(x,y);
}
预处理时:
/* main.cpp */
double add(double x, double y);//此处的#include"math.h"被改文件的代码所替换,并被一起送往编译器被编译

int main(){
double x = 1.00,y = 2.00;
add(x,y);
}

头文件

仅仅只是用于各种数据类型的申明(也就是各个某定义文件的原型——调用接口),并不参与定义。

实现文件通常具有文件扩展名 .cpp.cxx。 头文件通常具有扩展名 .h.hpp

但是需要注意的是:

在模板数据类型时,它处于头文件是定义形式。(即非申明形式,而是定义,形如内联函数)

1.可以写const对象为定义形式

因为const对象的作用域只会处于被包含的那个文件中,

2.可以写内联函数为定义形式

因为编译器在遇inline function时,不像common function,需要先申明再定义,而是需要先看该内联函数完整的定义。

3.可以写类为定义形式

因为类具有属性和函数,所以在头文件里,需要使用定义形式,将它们写进其中。

命名空间

使用using namespace 命名;这一语句,是为了将区别不同文件中同一名称的函数。

例如:若有文件a,和文件b,它们都有一个同名函数nsp(),为了区分不同,如下操作:

/*a_file.cpp*/
namespace a{
void nsp(){
    
	}       
}

/*b_file.cpp*/
namespace b{
void nsp(){
    
	}       
}

/*main.cpp*/
#include "b_file.cpp"
using namespace a;
#include "a_file.cpp"
using namespace b;
//当时用a_file中的nsp函数时
a::nsp();
//当时用b_file中的nsp函数时
b::nsp();
//使用命名空间,构建格式
namespace name命名空间名{
类、变量、函数等
}

working process of 编译器(编译器的工作过程)

第一步是:预处理阶段

将源代码中的宏进行替换

第二步是:编译阶段

编译器首先检查源代码的语法错误,当没有问题时,会将改文件编译为汇编语言再到一个二进制的目标文件。

第四步是:连接阶段

将上一步生成的目标文件,根据一些参数,连接生成最终的可执行文件。

其本质就是:将各个目标文件,按照一定要求(一般就是源文件中宏语句的代码位置)合并到同一个文件中。

头文件如何关联源文件?

已知头文件“a.h”声明了一系列函数(仅有函数原型,没有函数实现),“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢?

其实在预编译阶段,c.cpp和b.cpp都只是将宏语句进行替换,此处就是用头文件的申明代码将文件中的宏代码替换掉,以及编译阶段,c.cpp和b.cpp都只是检查语法错误,能够通过编译标准,并生成目标文件(.obj),那么在连接阶段,才会在c.cpp生成的c.obj目标文件中查找调用其他目标文件的部分(如:调用的b.cpp的部分),然后去连接b.cpp生成的b.obj文件的中调用的那部分,并build为可执行文件。

变量和函数命名规则

  1. 在编译器中命名可以由数字、字母和下划线组成;但是不能由数字开头;以及使用单下划线和双下划线作开头的命名资源要留给编译器用于实现全局标识符。
  2. 需要区分大小写和不使用c++关键字。

例如:

#include<string>
int FirstVariable = 1;
double Seconde_Variable =2.00;
string thridVariable = "3";
unsigned long Forthvariable = 4;

链接规则

链接的类型指定对象的名称是仅在一个文件中可见,还是在所有文件中可见。

注意:

链接的概念仅适用于全局名称。 链接的概念不适用于在一定范围内声明的名称。 范围是由一组封闭的大括号指定的,例如在函数或类的定义中。

如果要强制一个全局名称具有内部链接,可以将它显式声明为 static。 此关键字将它的可见性限制在声明它的同一翻译单元内。 在此上下文中,static 表示与应用于局部变量时不同的内容。

默认情况下,以下对象具有内部链接:

  • const对象
  • constexpr对象
  • typedef对象
  • 命名空间范围中的 static 对象

若要为 const 对象提供外部链接,请将其声明为 extern 并为其赋值: extern const a = 5 ;

为函数进行extern申明,使其告诉编译器,能够被其他cpp文件调用。

/*temp1.cpp*/
double ADD(double a, double b) {
	return a + b;
}
/*main.cpp*/
#include<iostream>
#include<string>
extern double ADD(double, double);

int main() {
	constexpr auto num1 = 2.09,num2 = 7.42;
	auto num3 = ADD(num1, num2);
	std::cout << num3 << std::endl;
}
//使用extern注意事项:
//被声明为extern 的函数或者全局变量,其实在本cpp中也可以定义
// extern 的作用:告诉编译器,在某个cpp文件中,存在这么一个函数或者全局变量
//所以在本cpp其实也是可以定义的

const和constexpr关键字

即凡是表达“只读”语义的场景都使用 const,初始化不允许再次被更改。

表达“常量”语义的场景都使用 constexpr。

constconstexpr 变量之间的主要 difference(区别)是,const 变量的初始化可以推迟到运行时进行。 constexpr 变量必须在编译时进行初始化。 所有的 constexpr 变量都是 const

const 不同,constexpr 也可以应用于函数和类 constructor(构造函数)。 constexpr 指示值或返回值是 constant(常数),如果可能,将在编译时进行计算。

constexpr 函数

constexpr 函数是在使用需要它的代码时,可在编译时计算其返回值的函数。 使用代码需要编译时的返回值来初始化 constexpr 变量,或者用于提供非类型模板自变量。 当其自变量为 constexpr 值时,函数 constexpr 将生成编译时 constant(常数)。 使用非 constexpr 自变量调用时,或者编译时不需要其值时,它将与正则函数一样,在运行时生成一个值。 (此双重行为使你无需编写同一函数的 constexpr 和非 constexpr 版本。)

constexpr 函数或 constructor(构造函数)通过隐式方式 inline

以下规则适用于 constexpr 函数:

  • constexpr 函数必须只接受并返回文本类型
  • constexpr 函数可以是递归的。
  • 它不能是虚拟的。 当封闭类具有任何虚拟基类时,不能将 constructor(构造函数)定义为 constexpr
  • 主体可以定义为 = default= delete
  • 正文不能包含如何 goto 语句或 try 块。
  • 可以将非 constexpr 模板的显式专用化声明为 constexpr
  • constexpr 模板的显式专用化不需要同时也是 constexpr

以下规则适用于 Visual Studio 2017 及更高版本中的 constexpr 函数:

  • 它可以包含 ifswitch 语句,以及所有循环语句,包括 for、基于范围的 forwhile、和 do-while。
  • 它可能包含局部变量声明,但必须初始化该变量。 它必须是文本类型,不能是 static 或线程本地的。 本地声明的变量不需要是 const,并且可以变化。
  • constexprstatic 成员函数不需要通过隐式方式 const

例子:

#include<iostream>
constexpr int ADD2()
{
	return 2;
}
int main() {
	constexpr auto num1 = 2.09,num2 = 7.42;


	constexpr auto num5 = ADD2() * 10;
	std::cout << num5 << std::endl;

}
 //因为constexpr函数,需要使用代码在编译器编译时,返回常量结果,所以它的定义,应在使用它的代码之前
//如上所示,常见的constexpr函数在分析时被看作内联函数。

线程和静态对象的销毁

直接调用 exit 时(或在 mainreturn 语句之后调用它时),将销毁与当前线程关联的线程对象。 然后,按与初始化相反的顺序销毁静态对象(在调用指定给 atexit 的函数(如果有)之后)。

在下面的示例中,在进入 main 之前,将创建和初始化静态对象 sd1sd2。 使用 return 语句终止此程序后,首先销毁 sd2,然后销毁 sd1ShowData 类的析构函数将关闭与这些静态对象关联的文件。

// using_exit_or_return1.cpp
#include <stdio.h>
class ShowData {
public:
   // Constructor opens a file.
   ShowData( const char *szDev ) {
   errno_t err;
      err = fopen_s(&OutputDev, szDev, "w" );
   }

   // Destructor closes the file.
   ~ShowData() { fclose( OutputDev ); }

   // Disp function shows a string on the output device.
   void Disp( char *szData ) {
      fputs( szData, OutputDev );
   }
private:
   FILE *OutputDev;
};

//  Define a static object of type ShowData. The output device
//   selected is "CON" -- the standard output device.
ShowData sd1 = "CON";

//  Define another static object of type ShowData. The output
//   is directed to a file called "HELLO.DAT"
ShowData sd2 = "hello.dat";

int main() {
   sd1.Disp( "hello to default device\n" );
   sd2.Disp( "hello to file hello.dat\n" );
}

alignofalignas

alignas 类型说明符是一种可移植的 C++ 标准方法,用于指定变量和用户定义类型的自定义对齐方式。 alignof 运算符同样也是一种标准的、可移植的方法,可以获取指定类型或变量的对齐方式。

指定的是该简单数据结构或者复合数据结构的成员,在内存是如何对齐(占用空间大小)。

// alignas_alignof.cpp
// compile with: cl /EHsc alignas_alignof.cpp
#include <iostream>
//此处结构体Bar,被关键字alignas(num)指定了内存大小为16字节。
struct alignas(16) Bar
{
    int i;       // 4 bytes
    int n;      // 4 bytes
    alignas(4) char arr[3];
    short s;          // 2 bytes
};

int main()
{
    std::cout << alignof(Bar) << std::endl; // output: 16
}

标准布局类型

当类或结构不包含某些 C++ 语言功能(例如无法在 C 语言中找到的虚拟函数),并且所有成员都具有相同的访问控制时,该类或结构为标准布局类型。 可以在内存中对其进行复制,并且布局已经过充分定义,可以由 C 程序使用。 标准布局类型可以具有用户定义的特殊成员函数。 此外,标准布局类型还具有以下特征:

  • 没有虚拟函数或虚拟基类
  • 所有非静态数据成员都具有相同的访问控制
  • 类类型的所有非静态成员均为标准布局
  • 所有基类都为标准布局
  • 没有与第一个非静态数据成员类型相同的基类。
  • 满足以下条件之一:
    • 最底层派生类中没有非静态数据成员,并且具有非静态数据成员的基类不超过一个,或者
    • 没有含非静态数据成员的基类

值类型和移动效率

移动在适当的成员函数声明和定义移动构造函数和移动赋值方法中使用双与号 (&&) rvalue 引用。 还需要插入正确的代码,以从源对象中“窃取内容”。

如何确定是否需要启用移动操作? 如果你已经知道需要启用复制构造,你可能也希望启用移动构造,特别是它比深层副本便宜的情况。 但是,如果知道需要移动支持,这不一定意味着要启用复制操作。 后一种情况称为“仅移动类型”。 标准库中已有的示例是 unique_ptr。 顺便说一下,旧的 auto_ptr 已被弃用,并被 unique_ptr 取代,这正是由于以前版本的 C++ 中缺乏移动语义支持。

通过使用移动语义,可以按值返回或在中间插入。 移动是复制的优化。 无需将堆分配作为解决方法。 请看下面的伪代码:

#include <set>
#include <vector>
#include <string>
using namespace std;

//...
set<widget> LoadHugeData() {
    set<widget> ret;
    // ... load data from disk and populate ret
    return ret;
}
//...
widgets = LoadHugeData();   // efficient, no deep copy

vector<string> v = IfIHadAMillionStrings();
v.insert( begin(v)+v.size()/2, "scott" );   // efficient, no deep copy-shuffle
v.insert( begin(v)+v.size()/2, "Andrei" );  // (just 1M ptr/len assignments)
//...
HugeMatrix operator+(const HugeMatrix& , const HugeMatrix& );
HugeMatrix operator+(const HugeMatrix& ,       HugeMatrix&&);
HugeMatrix operator+(      HugeMatrix&&, const HugeMatrix& );
HugeMatrix operator+(      HugeMatrix&&,       HugeMatrix&&);
//...
hm5 = hm1+hm2+hm3+hm4+hm5;   // efficient, no extra copies

为适当的值类型启用移动

对于类似于值的类,移动比深度副本便宜,启用移动构造和移动赋值以提高效率。 请看下面的伪代码:

#include <memory>
#include <stdexcept>
using namespace std;
// ...
class my_class {
    unique_ptr<BigHugeData> data;
public:
    my_class( my_class&& other )   // move construction
        : data( move( other.data ) ) { }
    my_class& operator=( my_class&& other )   // move assignment
    { data = move( other.data ); return *this; }
    // ...
    void method() {   // check (if appropriate)
        if( !data )
            throw std::runtime_error("RUNTIME ERROR: Insufficient resources!");
    }
};

类型转换和类型安全

  • static_cast,用于仅在编译时检查的强制转换。 如果编译器检测到你尝试在完全不兼容的类型之间强制转换,static_cast 将返回错误。 您还可以使用它在指向基对象的指针和指向派生对象的指针之间强制转换,但编译器无法总是判断出此类转换在运行时是否安全。

    C++复制

    double d = 1.58947;
    int i = d;  // warning C4244 possible loss of data
    int j = static_cast<int>(d);       // No warning.
    string s = static_cast<string>(d); // Error C2440:cannot convert from
                                       // double to std:string
    
    // No error but not necessarily safe.
    Base* b = new Base();
    Derived* d2 = static_cast<Derived*>(b);
    

    有关详细信息,请参阅 static_cast

  • dynamic_cast,用于从指向基对象的指针到指向派生对象的指针的、安全且经过运行时检查的强制转换。 dynamic_cast 在向下转换方面比 static_cast 更安全,但运行时检查会产生一些开销。

    C++复制

    Base* b = new Base();
    
    // Run-time check to determine whether b is actually a Derived*
    Derived* d3 = dynamic_cast<Derived*>(b);
    
    // If b was originally a Derived*, then d3 is a valid pointer.
    if(d3)
    {
       // Safe to call Derived method.
       cout << d3->DoSomethingMore() << endl;
    }
    else
    {
       // Run-time check failed.
       cout << "d3 is null" << endl;
    }
    
    //Output: d3 is null;
    

    有关详细信息,请参阅 dynamic_cast

  • const_cast,用于转换掉变量的 const 性,或者将非 const 变量转换为 const。 使用此运算符转换掉 const 性与使用 C 样式强制转换一样容易出错,只不过使用 const_cast 时不太可能意外地执行强制转换。 有时候,必须转换掉变量的 const 性。例如,将 const 变量传递给采用非 const 参数的函数。 以下示例演示如何执行此操作。

    C++复制

    void Func(double& d) { ... }
    void ConstCast()
    {
       const double pi = 3.14;
       Func(const_cast<double&>(pi)); //No error.
    }
    

    有关详细信息,请参阅 const_cast

  • reinterpret_cast,用于无关类型(如指针类型和 int)之间的强制转换。

    备注

    此强制转换运算符不像其他运算符一样常用,并且不能保证可将其移植到其它编译器。

    以下示例演示 reinterpret_caststatic_cast 的差异。

    C++复制

    const char* str = "hello";
    int i = static_cast<int>(str);//error C2440: 'static_cast' : cannot
                                  // convert from 'const char *' to 'int'
    int j = (int)str; // C-style cast. Did the programmer really intend
                      // to do this?
    int k = reinterpret_cast<int>(str);// Programming intent is clear.
                                       // However, it is not 64-bit safe.
    

有关详细信息,请参阅 reinterpret_cast 运算符

nullptr

关键字 nullptr 指定类型 std::nullptr_t 的 null 指针常量,该类型可转换为任何原始指针类型。 尽管可以使用关键字 nullptr 而不包含任何标头,但如果代码使用类型 std::nullptr_t,则必须通过包含标头 <cstddef> 来定义该类型。

备注

用于托管代码应用程序的 C++/CLI 中也定义了 nullptr 关键字,并且它与 ISO 标准 C++ 关键字不可互换。 如果可以使用 /clr 编译器选项(以托管代码为目标)编译代码,则在必须保证编译器使用本机 C++ 解释的任何代码行中使用 __nullptr。 有关详细信息,请参阅 nullptr(C++/CLI 和 C++/CX)

注解

请避免将 NULL 或零 (0) 用作 null 指针常量;nullptr 不仅不易被误用,并且在大多数情况下效果更好。 例如,给定 func(std::pair<const char *, double>),那么调用 func(std::make_pair(NULL, 3.14)) 会导致编译器错误。 宏 NULL 将扩展到 0,以便调用 std::make_pair(0, 3.14) 将返回 std::pair<int, double>,此结果不可转换为 funcstd::pair<const char *, double> 参数类型。 调用 func(std::make_pair(nullptr, 3.14)) 将会成功编译,因为 std::make_pair(nullptr, 3.14) 返回 std::pair<std::nullptr_t, double>,此结果可转换为 std::pair<const char *, double>

RAII 原则

资源获取即初始化.

资源(堆内存、文件句柄、套接字等)应由对象“拥有”。 该对象在其构造函数中创建或接收新分配的资源,并在其析构函数中将此资源删除。 RAII 原则可确保当所属对象超出范围时,所有资源都能正确返回到操作系统。

C++类型

标量类型

保存已定义范围的单个值的类型。 标量包括算术类型 (整型或浮点值) 、枚举类型成员、指针类型、指向成员的指针类型和 std::nullptr_t。 基本类型通常是标量类型。

复合类型

不是标量类型的类型。 复合类型包括数组类型、函数类型、类 (或结构) 类型、联合类型、枚举、引用和指向非静态类成员的指针。

变量

数据数量的符号名称。 该名称可用于访问它所引用的整个代码范围内的数据。 在 C++ 中, 变量 通常用于引用标量数据类型的实例,而其他类型的实例通常称为 对象

引用变量

引用变量相当于给变量起的别名(也就是两者在内存中的地址相同);例如:

int a = 5;
int & b = a;//此时的b为a的别名
std::cout << &num3 << '\n' <<& cont << std::endl;
>>
num3的地址:000000E9700FFB14
cont的地址:000000E9700FFB14
//可看出其引用类型的地址与被引用变量地址相同

需要注意的是,它与地址的操作不同。指针变量类型,传递的是地址其指针变量本身地址与存储地址不同。如下:

	int num3 = 5;
	int* ptr = &num3;
	std::cout << &ptr << '\n' << &num3 << std::endl;
>>
ptr的地址:00000090A015F768
num3的地址:00000090A015F704

对象

为简单起见和一致性,本文使用术语 对象 来引用类或结构的任何实例。 在一般意义上使用它时,它包括所有类型的变量,甚至标量变量。

POD 类型(纯旧数据):

C++ 中的此类非正式数据类型类别是指作为标量(参见基础类型部分)的类型或 POD 类。 POD 类没有非 POD 的静态数据成员,也没有用户定义的构造函数、用户定义的析构函数或用户定义的赋值运算符。 此外,POD 类无虚函数、基类、私有的或受保护的非静态数据成员。 POD 类型通常用于外部数据交换,例如与用 C 语言编写的模块(仅具有 POD 类型)进行的数据交换。

基本数据类型

类型 大小 评论
int 4 个字节 整数值的默认选择。
double 8 字节 浮点值的默认选择。
bool 1 个字节 表示可为 true 或 false 的值。
char 1 个字节 用于早期 C 样式字符串或 std:: 字符串对象中无需转换为 UNICODE 的 ASCII 字符。
wchar_t 2 个字节 表示可能以 UNICODE 格式进行编码的“宽”字符值(Windows 上为 UTF-16,其他操作系统上可能不同)。 wchar_t 是在 类型字符串中使用的字符类型 std::wstring
unsigned char 1 个字节 C++ 没有内置字节类型。 使用 unsigned char 来表示字节值。
unsigned int 4 个字节 位标志的默认选项。
long long 8 字节 表示更大的整数值范围。

数字

整型

八进制数:以0开头,十六进制的数:以0x开头。 千位分隔符`,在较大值的数值里:1`234`456`1678

若要指定无符号类型,请使用 uU 后缀。 若要指定 long 类型,请使用 lL 后缀。 要指定 64 位整型类型,请使用 LL ll 后缀。 i64 后缀仍受支持,但不建议使用。 它是 Microsoft 专用的,不可移植。

浮点数

可以使用科学计数法来表示:18.46e1=184.6 18.46e-1 = 1.846

关键字nullptr

表示空指针常量,即null.

字符串和字符文本

字符串文本可以米有前缀,也可以具有 u8LuU 前缀以分别指示窄字符(单字节或多字节)、UTF-8、宽字符(UCS-2 或 UTF-16)、UTF-16 和 UTF-32 编码。 原始字符串文本可以具有 Ru8RLRuRUR 前缀来表示这些编码的原始版本等效项。 若要创建临时或静态 std::string 值,可以使用带 s 后缀的字符串文本或原始字符串文本。

字符文本

    auto c1 = u8'A'; // char
    auto c2 =  L'A'; // wchar_t
    auto c3 =  u'A'; // char16_t
    auto c4 =  U'A'; // char32_t

字符类型检测

isalpha()检测是否为字母字符;

isdigits()检测是否为数字;

isspace()检测是否为空白;

ispunct()检测是否为标点符号。

如果检测符合,则返回非零值。

字符串

	auto s1 = u8"hello"; // const char* before C++20, encoded as UTF-8,
                         // const char8_t* in C++20
    auto s3 =  u"hello"; // const char16_t*, encoded as UTF-16
    auto s4 =  U"hello"; // const char32_t*, encoded as UTF-32

用户自定义文本类型

ReturnType operator "" _t( basic data_type);  
返回类型 operator "" _suffi(自定义的后缀名) (参数类型)

以下为实例:

// UDL_Distance.cpp

#include <iostream>
#include <string>

struct Distance
{
private:
    explicit Distance(long double val) : kilometers(val)
    {}
//加了friend关键字,为友元函数,其私有属性也可被友元函数访问。
    friend Distance operator"" _km(long double val);
    friend Distance operator"" _mi(long double val);

    long double kilometers{ 0 };
public:
    const static long double km_per_mile;
    long double get_kilometers() { return kilometers; }

    Distance operator+(Distance other)
    {
        return Distance(get_kilometers() + other.get_kilometers());
    }
};

const long double Distance::km_per_mile = 1.609344L;
//用户自定义文本,不仅为一个字符文本,还可以有对应操作。
Distance operator"" _km(long double val)
{
    return Distance(val);
}

Distance operator"" _mi(long double val)
{
    return Distance(val * Distance::km_per_mile);
}

int main()
{
    // Must have a decimal point to bind to the operator we defined!
    Distance d{ 402.0_km }; // construct using kilometers
    std::cout << "Kilometers in d: " << d.get_kilometers() << std::endl; // 402

    Distance d2{ 402.0_mi }; // construct using miles
    std::cout << "Kilometers in d2: " << d2.get_kilometers() << std::endl;  //646.956

    // add distances constructed with different units
    Distance d3 = 36.0_mi + 42.0_km;
    std::cout << "d3 value = " << d3.get_kilometers() << std::endl; // 99.9364

    // Distance d4(90.0); // error constructor not accessible

    std::string s;
    std::getline(std::cin, s);
    return 0;
}

复杂数据类型

指针

原始指针

使用"*"星号,定义一个原始指针变量,这是一个内存存储的地址的变量,并非存储的数据。

并且在使用时,需要初始化:即赋予一个地址

int num = 10;
int* a = &num;
cout << *a << endl;//在指针变量前加个"*",是取存储的地址所存储的内容 *a == num
>> 10

需要注意的是:内存中只是为其分配一个足够大的空间存储地址

“&”引用符号:取变量地址。

在进行开辟堆空间时,我们使用new关键字,malloc()c的库函数,在不需要之后,我们需要删掉堆中的空间:使用delete关键字,free()C库函数。但是,我们一般会忘记,所以引出智能指针

前缀和后缀自增减

	double arr[5] = {1.1,2.2,3.3,4.4,5.5};
	double* pt = arr;
	++pt;
	cout << *++pt << endl;
	cout << *pt++ << endl; // FirstStep: out stream inserted variable: *pt ; SecondStep: point remove index d = 1: pt = pt + 1 ;
	cout << *pt << endl;
>> *++pt = 3.3
   *pt++ = 3.3
   *pt = 4.4

前缀自增指针:先移动指针再给出内容;后缀自增指针:先给出内容再移动指针。

	int a[3] = {1,3,5};
	int *p = a;
	cout << ++ *p << *p; //先取a[0]的内容,再修改了a[0]的值,指针不移动
	cout << endl;
	cout <<  *++p << *p; //先移动指针到a[1],再取其内容;
	cout << endl;
	cout <<    *p ++ <<*p;//先返回a[1]的内容,再移动指针到a[2]
	

指针常量

又称为指针类型为常量。

int a =1;
int* const arr = &a; //指针常量定义时就必须初始化
//此时的arr为指针常量,不可修改保存的地址,但可修改其存储变量的内容
int b = 2;
arr1 = &b;//not allowed -error,不可以修改指针常量存储的地址
*arr1 = 3;//allowed,可以修改指针存储的变量的内容

常量指针

又称指向“常量”的指针。

int a = 1;
const int* arr1 = &a; // == int*arr1 =&const int a ;
//此时arr1为常量指针,其修改arr1保存的地址可以,但修改arr1保存的地址的内容不可以
int b = 2;
arr1 = &b;//allowed,可以修改指针存储的地址
*arr1 = 3;//not allowed -error,不可以修改指针存储对应变量的内容
//当时常量的地址想用指针存储时,必须使用常量指针变量存储,当然仍然只有其存储变量的内容不可更改。
const int a= 1;
const int* pt = &a;
int b = 2;
pt = &b;//允许
*pt = 3;//不被允许 

常量指针常量

//常量指针常量,其两者都不可更改
	int a = 1;
	const int* const pt = &a;
	int b = 2;
pt =&b;//不被允许
*pt = b;//不被允许

拓展(接指针常量和常量指针的内容)

//间接修改const变量的值
const int** pt2;
int* pt1;
const int a=1;
pt2 = &pt1;
pt1 = &a;
*pt1 = 10;//allowed被允许,但是修改了常量变量。

例题:

判断下面程序对错,并说明理由

int main()
{
    char * const str = "apple";
    * str = "orange";//此时想修改字符串常量“apple”是不被允许的
    cout << str << endl;
    getchar();
}

错误:

"apple"是字符串常量放在常量区,str指向"apple",那么str指向的是字符串常量"apple"的首地址,也就是字符a的地址,因此str指向字符a,str就等于字符a,对str的修改就是对字符串首字符a的修改,但"apple"是一个字符串常量,常量的值不可修改。

根据字符串赋值规则,可以修改整个字符串,方法是对指向字符串的指针str进行赋值,如下:

str = "orange";//此时是修改指针常量存储的地址不被允许

但依旧是错误的,在该赋值语句中,系统会在常量区一块新的空间写入字符串"orange"并返回其首地址,此时str由指向字符串常量"apple"的首地址变为指向字符串常量"orange"的首地址,str指向的地址发生了变化,但str是指针常量不能被修改,所以错误。

如果想要程序编译通过,就不能将str声明为指针常量,否则str在初始化之后就无法修改。因此将const修饰符去掉,并修改字符串赋值语句,修改后程序如下:

int main()
{
    char * str = "apple";
    str = "orange";
    cout << str << endl;
    getchar();
}

智能指针

指针被封装在一个对象中。如下:

void UseRawPointer()
{
    // Using a raw pointer -- not recommended.
    Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); 

    // Use pSong...

    // Don't forget to delete!
    delete pSong;   
}


void UseSmartPointer()
{
    // Declare a smart pointer on stack and pass it the raw pointer.
    unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));

    // Use song2...
    wstring s = song2->duration_;
    //...

} // song2 is deleted automatically here.
  • unique_ptr
    只允许基础指针的一个所有者。 除非你确信需要 shared_ptr,否则请将该指针用作 POCO 的默认选项。 可以移到新所有者,但不会复制或共享。 替换已弃用的 auto_ptr。 与 boost::scoped_ptr 比较。 unique_ptr 小巧高效;大小等同于一个指针且支持 rvalue 引用,从而可实现快速插入和对 C++ 标准库集合的检索。 头文件:<memory>。 有关详细信息,请参阅如何:创建和使用 unique_ptr 实例unique_ptr 类
  • shared_ptr
    采用引用计数的智能指针。 如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。 直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。 大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。 头文件:<memory>。 有关详细信息,请参阅如何:创建和使用 shared_ptr 实例shared_ptr 类
  • weak_ptr
    结合 shared_ptr 使用的特例智能指针weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,需要断开 shared_ptr 实例间的循环引用。 头文件:<memory>。 有关详细信息,请参阅如何:创建和使用 weak_ptr 实例weak_ptr 类

**注意:智能指针需要多学习

使用智能指针创建动态数组

指针数组

指针的数组:也就是说,数组的元素是指针

int *a[3];//此时的数组有三个int型的指针

数组指针

数组的指针:也就是说这个数组的首地址存放在这个指针中。

int (*a)[3];//此时的指针a存放这个数组的首地址

两者加深理解

	int a = 1, b = 2, c = 3;

	int* arr [3] ={&a,&b,&c};
	int* (*arr1)[3] = &arr;
	cout << "指针数组指针" << endl;
	cout <<"&a:"<< arr[0] << endl;
	cout << "a:" << *arr[0] << endl;

	cout << "指针数组的指针:int* (*arr1)[]:" << endl;
//为指针数组这个变量的地址
	cout << "(*arr1):" << *arr1<<"=&arr:"<< & arr << endl;
//为指针数组的指针元素(int*)的地址
	cout << "&(*arr1)[0]:" << &(*arr1)[0] << "=&arr[0]:" << &arr[0] << endl;
//这才是为a的真正地址
	cout << "*(*arr1)[0]:" << &*(*arr1)[0] << "=*arr[0]" << &*arr[0] << endl;
	cout << "&a:" <<& a<<endl;

>>
指针数组指针
&a:000000B1AAAFFA14
a:1
指针数组的指针:int* (*arr1)[]:
(*arr1):    000000B1AAAFFA78 =&arr:   000000B1AAAFFA78
&(*arr1)[0]:000000B1AAAFFA78 =&arr[0]:000000B1AAAFFA78
*(*arr1)[0]:000000B1AAAFFA14 =*arr[0]:000000B1AAAFFA14
&a:000000B1AAAFFA14 == &arr[0]//因为arr[0]存储的是a的地址

数组

原始数组定义

int a[5] = {1,2,3,4,5};
//原始数组计算元素个数
sizeof variable / sizeof (type_data);
sizeof a / sizeof(int);

动态数组

int *array =  new int[10];//1-dimension
//在使用动态数组时,我们应使用同一个类型指针保存动态数组的首地址
//禁止在动态数组最原先保存首地址的指针上操作,防止该动态数组首地址改变。

如果是为new[]生成的动态数组释放空间,使用delete[]。

如果是使用new[]为一个对象分配内存,则应使用delete来释放。

object * p = new object[nums];
delete[] p;

在数组初始化时需要注意

数组内的元素是不能进行缩窄转换的,如int型数组内部有一个double型元素。

C-风格的字符串

//有以下形式:
//但需要注意的是,在此时第一个形式的字符串定义中,不能出现单个空字符为元素,是不被编译器允许的,这样会是该字符串被截断不完整。 
//字符数组是需要自己加上结束符的。

//字符数组
char a[6]={'h','e','l','l','o','\0'};

//字符串常量
char a[] = "hello";//但以下形式使用“”双引号,则初始化可以省略结束符。
 a[2] = *(a+2);
//字符串指针
char* a = new char[5];

例题:

//表达式*"pizza"的含义是什么?"taco”[2]呢?
//"pizza",表示的是字符串常量的首地址,*"pizza"那就是地址对应的值:'p'
//"taco"[2]==ch[2]==*(字符串常量地址+2)>>c;

长度计算

数组计算长度是需要使用数组名,而不是存储首地址的中间指针变量。

sizeof()

sizeof返回的是任意类型数组的长度,在字符串中包含‘\0’结束符

sizeof variable / sizeof (type_data);
strlen()

strlen是只能返回的字符数组中字符串的长度,不包含结束符。

#include<sting>
using namespace std;
strlen(variable);

字符数组连接和复制

复制

//strcpy_s(charr1, charr2)来复制字符数组//将charr2的内容复制到charr1中
//为什么不使用strcpy,因为strcpy不会进行数组参数的长度检测,所以建议使用strcpy_s
strcpy_s(charr1,charr2);
//strnpy_s的size,限定了charr1最多赋值多少长度的字符数
strnpy_s(charr1,charr2,size)

连接

//strcat_s(charr1, charr2)来将字符数组进行连接 //将charr2的内容连接到charr1中
strcat_s(charr1, charr2);

两个字符数组进行连接

需要知道的是,依次访问字符数组元素,当访问到结束符时循环会自动停止

例子:

#include<iostream>
using namespace std;
void char_insert(char* array1, char* array2);
int main(){
char Firstname[20];
	char Secondname[20];

	cout << "What is your first name ?" ;
	cin.getline(Firstname,20);
	cout << "What is your last name ?";
	cin.getline(Secondname, 20);
	char name[50]="\0";//需要先给name字符数组一个初始化字符'\0'
	char_insert(name, Secondname);

	char temp[3]{ ',',' '};
	char_insert(name, temp);
	char_insert(name, Firstname);
	
    cout << name;
    //此下循环只是展示字符数组访问元素什么时候停止。
	/*cout << "Name:";
	for (char* t = name; *t; ++t) cout << *t;
	cout << endl;*/
}


void char_insert(char* array1, char* array2) {
	//char* p= array1;
	//char* p2 = array2;
	//指针移动到结束符
	while (*array1)array1++;
    //此处的array不能放入到判断语句中,因为此处只有在array1的指针没有移动到结束字符处,才会向下移动。
    //而若是*array1++,则会出现已经移动到结束字符处,还会移动到一下地方,导致array2没有存放到正确的位置。
	while (*array1++ = *array2++);
}

逐过程:跳过进入函数体的过程,直接进行下一句;逐语句:会进入到函数体。

字符进行比较

strcmp(str1,str2),是进行字符串(字符数组)比较,实际是比较其每个字符的ascii码值

	char a[4] = "abc";
	char b[4] = "def";
	char c[4] = "abc";
当str1==str2时strcmp函数返回0
	strcmp(a,c) -> return 0;
当str1>str2时strcmp函数返回正数1
    strcmp(b,a) -> return 1;
当str1<str2时strcmp函数返回负数-1
    strcmp(a,b) -> return -1;

而string类型的比较,可直接使用==、!=、<、>关系符号进行比较。

string类型

该类型是c++98提出,并且相比于字符数组更具有安全性。

#include<sting>
using namespace std;
int main(){
	string str1 = "123";
	string str2 ;
    //类似于cin.getline(),但是因为iostream类中没有对string对象处理的类方法。
	getline(cin, str2); 
    
	cout << str1 + str2 << endl;// link :str1 + str2
	str1 = str2; // str1 assignment str2
	cout << str1 << endl;
	
    cout << str1.size() << endl; //str1 length
	
    wchar_t titile[] = L"Chief Astrogator"; // wchar_t类型的字符串
	char16_t name[] = u"Felonia Ripova";    // char16_t类型的字符串
	char32_t car[] = U"Humber Super Snipe"; // char32_t类型的字符串
	//使用原始字符串R,"()"为字符串的起始和终止符,也可自定义。其作用是而无需加转义字符进行转换
    cout << R"*((asd)asd)*" << endl;
return 0;
}

C++98 STL:新类型vector

vector是可变大小的序列容器,具有动态分配能力,但是其插入和删除元素只能通过末尾。

#include<vector>
using namespace std;
vector<type_data>name [(nums)];//如果加了长度,会默认初始化添加长度个的0元素
//而如果使用的是{1,2,3,4}则是初始化添加值
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
//注意size_t是无符号long long型,是不会出现i<0的情况
	for (size_t i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
//对于容器中存放对象类型的迭代
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) ;

c++11 ,引入新类型array

array是序列式容器,类似于c语言的数组,是固定大小,一旦定义后是无法进行扩容和收缩。

#include<array>
using namespace std;
array<type_data,nums>name;
array<long, 3> array_test = {123,231,312};

迭代器遍历

函数 功能
begin 返回容器中第一个元素的迭代器
end 返回容器中最后一个元素之后的迭代器
rbegin 返回指向最后一个元素的迭代器
rend 返回指向第一个元素之前的迭代器
std::array<int, 4> arr = { 1, 3, 2, 4 };
std::cout << "arr values:" << std::endl;
for (auto it = arr.begin(); it != arr.end(); it++) {
	std::cout << *it << " ";
}
std::cout << std::endl;

array大小

函数 功能
size 返回 容器中当前元素的数量
max_size 返回容器可容纳的最大数量
empty 判断容器是否为空

array元素操作

函数 功能
at 返回容器在n处的引用,有错误检测
front 返回容器的第一个元素的值
back 返回容器的最后一个元素的值
data 返回指向首个元素的指针
fill 填充元素
swap 交换两个array容器内的元素

例如:

	array<int, 4>a ;
	for (int i = 0; i < a.size(); i++) {
		a.at(i) = i;
	}
	cout << "a init:";
	for (auto it = a.begin(); it != a.end(); it++) {
		cout << *it << " ";
	}
	cout <<  endl;

	array<int, 4> a1 = { 4,3,2,1 };
	a.swap(a1);
	cout << "a swap a1:";
	for (auto it = a.begin(); it != a.end(); it++) {
		cout << *it << " ";
	}
	cout << endl;
	a1.fill(0);
	cout << "a1.fill(0)";
	for (auto it = a1.begin(); it != a1.end(); it++) {
		cout << *it << " ";
	}
	cout << endl;

迭代器使用

array<char,50> array1;
//此处两个循环输出的是array数组的元素
for(auto i:array1){
    cout << i <<" ";
}
cout << endl;
==
    //需要知道的是,此时的迭代器对象item是一个指针类型
for(array<char,50>::iterator item= array1.begin(); item !=array1.end();++item){
//for(auto item = array1.begin(); *item != array1.back(); ++item)此循环作用的是输出array的有效元素。
    cout << *item<<" ";
}
cout <<endl;

结构体

typedef struct structname
{
//	结构体成员
 char name[20];
 std::string bname;
 structname* ptr;//指向本结构体指针
 int a : 4 //此处的冒号指定int型成员a的占用字符为4个字节
}bname,*name ;//紧跟在后,此时的bname其实为一个structname对象,但是我们通常理解为别名


//构建结构体
typedef struct structname
{
    //结构体成员
    
}bname;
==
 struct structname
{
    //结构体成员
    
};
typedef structname bname;

例如:

#include<iostream>
#include<string>
using namespace std;
//结构体申明
typedef struct car
{
	std::string name;
	double price;
	int nums;
}Car;

int main(void){
	//结构体定义(初始化)
    car car1 = {"fucous", 23423.76, 1};
    car car2;
    car2.name= "byd";
    car2.price = 21313.5;
    car2.nums = 1;
    car* car3;
    car3->name = "audi";
    car3->price = 42321.2;
    car3->nums = 2;
return 0;
}

共用体union

其特性是在使用时只有一个成员变量是实际存储在内存中。

#define MaxSize 99999
union name
{
    int id_int;
    char id_char[MaxSize];
}
name n1;
n1.id_int = 5;
n1.id_char = "花生";//此时先前的n1.id_int所存储的int=5,将被覆盖

匿名共用体

所有的成员变量共用一个内存地址。也就是说,每次只有一个成员为当前的成员。

struct car
{
    char brand[20];
    int type;
    union
    {
        long id_name;//使用编号赋予不同车厂的不同车类型
        std::string id_string;//使用字符串赋予存储不同车厂的不同车型
    }
    
}
...
Car car2;
if(car2.type == 1){
    //说明此时是卡车
   cin>>car2.id_name; 
}
...
else cin>> car2.id_string;

使用场景:系统或硬件数据结构。

C++11基于范围的for循环

当时用数组或容器类(vector,array类型)时,使用该形式的for循环,也可对其元素进行操作。

	vector<int> v(5);
	for (int &x:v)//此处使用引用&表明可修改序列的元素
	{
		x = x + 1;
	}
	for (int x : v) cout << x << " ";//此处不使用&,则表明不修改元素,仅读

特性

"friend"关键字

关键字"friend",是为了使本父级元素的私有的属性能被部分函数或类可访问。如下:

友元函数

当有函数被加以关键字"friend",那么该函数除了public attribute也可以访问所有的私有属性。

friend 返回类型 函数名(参数类型);

class CCar;  //提前声明CCar类,以便后面的CDriver类使用
class CDriver
{
public:
    void ModifyCar(CCar* pCar);  //改装汽车
    //此处的友元函数必定为public,否则没有访问私有属性的意义了
};
class CCar
{
private:
    int price;
    friend int MostExpensiveCar(CCar cars[], int total);  //声明友元
    friend void CDriver::ModifyCar(CCar* pCar);  //声明友元
	//故cDriver::ModifyCar()这个函数可以访问CCar类的所有属性
};

友元类

friend class 类名;

一个类A申明类B是自己的友元类,则类B可以访问类A的所有私有属性。

class CCar
{
	private:
		int price ;
		friend class CDriver;//这个友元类的定义,使得CDriver这个类可以访问类CCar
};
class CDriver
{
    CCar myCar;//生成CCar对象myCar
    public:
    	void MoDifyCar()
        {
        	myCar.price += 1000; //因为CDriver类为CCar类的友元类,所以,CDriver可以直接访问CCar类的属性。  
        }
}

注意:

友元关系在类之间不能传递,即类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元。“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上 不成立。

"auto"关键字

auto 关键字指示编译器使用已声明变量的初始化表达式或 lambda 表达式参数来推导其类型。

auto 关键字指示编译器通过初始值设定项推断类型。

建议:在变量类型复杂时使用最好。

auto a = 5; // typeid(a).name() >> int

先有(除了简单的数据类型)复合数据类型必须要有已知声明后auto才可推断出来。

const double* (*ap)(const double* arr,int size);//声明了一个函数指针
auto p2 = ap;//此时才生效

在大多情况下,建议使用 auto 关键字(除非确实需要转换),因为此关键字具有以下好处:

  • 鲁棒性: 如果表达式的类型已更改(包括函数返回类型发生更改时),则它只能正常工作。
  • 性能: 可以保证没有转换。
  • 可用性:不必担心类型名称拼写困难和拼写有误。
  • 效率:代码会变得更高效。

可能不需要使用 auto 的转换情况:

  • 需要特定类型,其他任何操作都不会执行任何操作。
  • 在表达式模板帮助程序类型中,例如 (valarray+valarray)

若要使用 auto 关键字,请使用它而不是类型来声明变量,并指定初始化表达式。 此外,还可通过使用说明符和声明符(如 constvolatile)、指针 (\*)、引用 (&) 以及右值引用 (&&) 来修改 auto 关键字。 编译器计算初始化表达式,然后使用该信息来推断变量类型。

auto 初始化表达式可以采用多种形式:

  • 通用初始化语法,例如 auto a { 42 };
  • 赋值语法,例如 auto b = 0;
  • 通用赋值语法,它结合了上述两种形式,例如 auto c = { 3.14159 };
  • 直接初始化或构造函数样式的语法,例如 auto d( 1.41421f );

有关详细信息,请参阅初始值设定项和本文档后面的代码示例。

auto 用于在基于范围的 for 语句中声明循环参数时,它使用不同的初始化语法,例如for (auto& i : iterable) do_action(i);。 有关详细信息,请参阅基于范围的 for 语句 (C++)

关键字 auto 是类型的占位符,但它本身不是类型。 因此, auto 关键字不能用于转换或运算符,例如 sizeof C++/CLI) 的 和 (typeid

有用性

auto 关键字是声明复杂类型变量的简单方法。 例如,可使用 auto 声明一个变量,其中初始化表达式涉及模板、指向函数的指针或指向成员的指针。

也可使用 auto 声明变量并将其初始化为 lambda 表达式。 您不能自行声明变量的类型,因为仅编译器知道 lambda 表达式的类型。 有关详细信息,请参阅 Lambda 表达式示例

function<int(int, int)> f2 = [](int x, int y) { return x + y; };
function<int (void)> f = [i, &j] { return i + j; };

尾部的返回类型

您可将 autodecltype 类型说明符一起使用来帮助编写模板库。 使用 autodecltype 声明函数模板,其返回类型取决于其模板参数的类型。 或者,使用 autodecltype 声明一个函数模板,该模板包装对另一个函数的调用,然后返回该函数的任何返回类型。 有关详细信息,请参阅 decltype

引用和 cv 限定符

使用 auto 将删除引用、 const 限定符和 volatile 限定符。 请考虑以下示例:

// cl.exe /analyze /EHsc /W4
#include <iostream>

using namespace std;

int main( )
{
    int count = 10;
    int& countRef = count;
    auto myAuto = countRef;

    countRef = 11;
    cout << count << " ";

    myAuto = 12;
    cout << count << endl;
}

在前面的示例中,myAuto 是 , int而不是 int 引用,因此输出为 11 11,而不是 11 12 ,如果引用限定符未由 auto删除,则不会如此。

范围

每个语句所作用的范围。

"::"范围解析运算符

"::"的左侧的名称使用被视为类名称。

隐藏类名

// hiding_class_names.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;

// Declare class Account at global scope.
class Account
{
public:
    Account( double InitialBalance )
        { balance = InitialBalance; }
    double GetBalance()
        { return balance; }
private:
    double balance;
};

double Account = 15.37;            // Hides class name Account

int main()
{	
    //隐藏类的方法
    class Account Checking( Account ); // Qualifies Account as
                                       //  class name

    cout << "Opening account with a balance of: "
         << Checking.GetBalance() << "\n";
}
//Output: Opening account with a balance of: 15.37

隐藏具有全局范围作用的名称

#include <iostream>

int i = 7;   // i has global scope, outside all blocks
using namespace std;

int main( int argc, char *argv[] ) {
   int i = 5;   // i has block scope, hides i at global scope
   cout << "Block-scoped i has the value: " << i << "\n";
   cout << "Global-scoped i has the value: " << ::i << "\n"; //使用范围解析运算符访问全局变量i
}
>> 
Block-scoped i has the value: 5
Global-scoped i has the value: 7

使用宏定义来进行一些免除重复性操作。例如:宏可以向函数一样被定义

详情见https://learn.microsoft.com/zh-cn/cpp/preprocessor/hash-if-hash-elif-hash-else-and-hash-endif-directives-c-cpp?view=msvc-170

部分宏定义和操作

#define

#define 宏定义名 宏操作//基本的宏定义
#define a(...) #__VA_ARGS__

#undef

#undef //取消宏定义,取消前面使用的宏定义

#ifdef、ifndef

等效于:
ifdef == if defined
ifndef ==if !defined

#if 、#elif、#else 、#endif

源文件中的每个 #if 指令必须与表示结束的 #endif 指令匹配。 任意数量的 #elif 指令可以出现在 #if 和 #endif 指令之间,但最多允许一个 #else 指令。 #else 指令(如果有)必须是 #endif 之前的最后一个指令。

#if、#elif、#else 和 #endif 指令可以嵌套在其他 #if 指令的 text 部分中。 每个嵌套的 #else、#elif 或 #endif 指令属于最靠近的前面的 #if 指令。

所有条件编译指令(如 #if 和 #ifdef)都必须在文件末尾之前匹配一个 #endif 关闭指令。 否则会生成错误消息。 当条件编译指令包含在包含文件中时,这些指令必须满足相同的条件:包含文件的末尾不能有未匹配的条件编译指令。

在 #elif 命令后面的行部分中执行宏替换,以便能够在 constant-expression 中使用宏调用。

预处理器选择 text 的给定匹配项之一以进行进一步处理。 text 中指定的块可以是文本的任意序列。 它可占用多个行。 通常,text 是对编译器或预处理器有意义的程序文本。

预处理器处理选定的 text,并将其传递给编译器。 如果 text 包含预处理器指令,则预处理器将执行这些指令。 仅编译预处理器所选的文本块。

预处理器通过计算每个 #if 或 #elif 指令后面的常量表达式来选择单个 text 项,直到找到实际(非零)常量表达式。 它选择所有文本(包括以 # 开头的其他预处理器指令),直到其关联的 #elif、#else 或 #endif。

如果 constant-expression 的所有匹配项都为 false,或者 #elif 指令未出现,则预处理器将选择 #else 子句后面的文本块。 如果没有 #else 子句,并且 #if 块中的 constant-expression 的所有实例都为 false,则不选择文本块。

constant-expression 是具有下列其他限制的整数常量表达式:

  • 表达式必须具有整型,并且只能包含整数常量、字符常量和 defined 运算符。
  • 表达式不能使用 sizeof 或 type-cast 运算符。
  • 目标环境无法表示整数的所有范围。
  • 转换以与类型 long 相同的方式表示类型 int,并以与类型 unsigned long 相同的方式表示类型 unsigned int
  • 转换器可以将字符常量转换为与目标环境的集不同的代码值集。 若要确定目标环境的属性,请使用为该环境生成的应用程序来检查 LIMITS.H 宏的值。
  • 该表达式不得查询环境,并且必须不受与目标计算机上的实现详细信息的影响。
#if defined(CREDIT)
    credit();
#elif defined(DEBIT)
    debit();
#else
    printerror();
#endif
/*
如果定义了标识符 credit,则编译对 CREDIT 的函数调用。 如果定义了标识符 DEBIT,则编译对 debit 的函数调用。 如果两个标识符都未定义,则编译对 printerror 的调用。 CREDIT 和 credit 都是 C 和 C++ 中的不同标识符,因为它们的情况不同。
*/

#line

#line //预处理时,设置行号和文件名
#line 20 "hello.cpp"
std::cout << __LINE__<<"\t"<<__FILE<<endl;
>> 20	hello.cpp

#error

#error //在编译时发出用户指定的错误消息,然后终止编译。

#pragma

指定计算机特定或操作系统特定的编译器功能

详情见:https://learn.microsoft.com/zh-cn/cpp/preprocessor/pragma-directives-and-the-pragma-keyword?view=msvc-170

变参宏

在宏定义中,类似于与python的可变参数(*arg).当用户调用该宏,则此参数是根据用户使用变化的

__VA_ARGS__
#define COUT(....) std::cout << __VA_ARGS__ << std::endl;
    
COUT(djy);
>>djy

单个#

在宏定义里,使用单个#,会将#后的变量数据格式变为字符换。

如:#define a (x) #x
在使用时,会将x变为字符串。

两个##

在宏定义里,其到的作用是连接,将##后的字符与前面连接

如:#define a(x) 1##x
cout << a(0) << endl;
>>输出为10

注意事项

在预扫描(prescan)时,复合宏里面的宏定义里都有#和##

那么在调用时,不会展开复合宏,反之,则会展开

如:
#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
#define TO_STRING( x ) TO_STRING1( x )
#define TO_STRING1( x ) #x

const char* str = TO_STRING(PARAM(ADDPARAM(1)));
cout << str << endl;
输出:
>> "ADDPARAM(1)"

宏调用自身

当宏调用自身时,不会展开,即将调用自身作参数直接输出

#define a(x)#x

a(a(2));
>>
    a(2)

匿名lambda函数

[捕获参数](参数列表) mutable//取消常量性 throw(异常处理) -> return-type返回类型 {函数体}

例如:

[捕获列表] (形参列表) ->返回值类型
{
	函数体;
}
int num = 100;
//此处的&用于引用具有全局范围的数据
auto fuction = [&](int a) ->void{
    num = 10;
    std::cout << num + a << std::endl;
};
fuction (2);
>> 12

throw异常处理此处需要学习。

枚举数据类型enum

定义

在c++中枚举数据类型是用户自定义的数据类型(在Python中类似迭代器一样的工作方式,其值是依次赋值)

//创建方式
// unscoped enum: 无范围限定的枚举类型
// enum [identifier] [: type] {enum-list};

// scoped enum: 有范围限定的枚举类型
// enum [class|struct] [identifier] [: type] {enum-list};

差异如下:限定范围的枚举需要使用对象调用形式。

//限定类型
namespace CardGame_Scoped
{
    enum class Suit { Diamonds, Hearts, Clubs, Spades };

    void PlayCard(Suit suit)
    {
        if (suit == Suit::Clubs) // Enumerator must be qualified by enum type
        { /*...*/}
    }
}

//无限定类型
namespace CardGame_NonScoped
{
    enum Suit { Diamonds, Hearts, Clubs, Spades };

    void PlayCard(Suit suit)
    {
        if (suit == Clubs) // Enumerator is visible without qualification
        { /*...*/
        }
    }
}
    

简答例子:

//无限定范围声明:
enum School_compent{Student_num, Teacher_nums, Other_nums }
//默认情况下 School_compent数据类型的枚举量为int型,
//编译器赋值:Student_num=0, Teacher_nums=1, Other_nums=2
//调用:直接使用名称

//有限定范围声明:
	enum class ad { Student_num=1, Teacher_nums, Other_nums };
//调用,在加入输出流时,需要将定义的ad类型强转为基础类型:int char
	std::cout << (int)ad::Other_nums<< std::endl;

不用枚举器的枚举:

​ 新类型是基础类型的精确复制,因此具有相同的调用约定,这意味着它可以跨 ABI 使用,而不会造成任何性能损失。 使用直接列表初始化初始化类型变量时,无需强制转换。

​ 通过使用此类型而不是其内置基础类型,可以消除无意隐式转换导致细微错误的可能。

enum class byte : unsigned char { };

OOP特性

函数

void函数

即无返回值函数,常用于打印值。

有返回值函数

其返回值可以是int、double、指针、结构或对象。但不能是数组(一般使用指针返回对数组操作后的首地址)。

其工作原理是:

main()函数执行到需要外部函数时,根据调用函数以及所带参数,语句执行进入其定义函数内部,依据语句块执行完毕,生成函数头定义的数据类型的返回值,于指定的cpu寄存器或内存单元中,然后告诉主函数,调用函数再寻找返回值,并赋值给同类型的数据变量。

函数原型

函数原型也叫函数声明,其定义在主函数前,头文件下

其这样做的行为,主要也是执行效率的提高。使编译器知道函数返回值类型;知道函数所需参数及类型。

函数原型中的参数列表,可以没有变量名,但必须要有数据类型。

int average(douhble ,int );

复杂数据类型作形参

一般来说复杂数据类型作形参,是传递的地址——也就是指针。所以当需要计算其长度时,应在调用函数的作用域中进行计算(比如主函数进行调用的函数其参数为数组和数组的长度,那么我需要计算数组的长度,传递进去才可以),因为变量的作用范围问题,在自身所属有效范围,sizeof求出的是整个数组,而当作实参传递后,在函数内部也只是一个int型的指针,其sizeof后,是不正确的。

#define Maxsize 8
double sum_arr(double *arr, int size);
int main(){
	double  arr[Maxsize] = {1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8};
	double sum = sum_arr(arr, Maxsize); //在调用函数之前sizeof(arr) /sizeof int== 8 

}
double sum_arr(double *arr,int size){
    //当在函数内部时,去求sizeof arr,则会得double*的长度:8
    double total=0.0;
    for(int i = 0;i<size;++i){
        total += arr[i];
    }
    return total;
}

注意:当不需要修改数组元素时,最好在函数的形参数据类型加上const

void funcion_test(const double* arr,int size){
    
};

访问数组方式

第一种:传递数组首地址和数组长度
void funcion_example(const double* arr,int size)
{
 	for(int i = 0; i< size ; ++i){
             std::cout << arr[i] << endl;   
    }   
}
第二种:传递数组首地址和尾地址
//调用
function_example(arr_head,arr_head+Maxsize)

//定义
void funcion_example(const double* arr_head,const double* arr_rear){
    const double* temp_ptr= arr_head;
    for(;temp_ptr != arr_rear;++temp_ptr){
        std::cout << *temp_ptr<<endl; 
    }
}

二维数组作形参

	int arr[3][4] = {
		{1,2,3,4},
		{5,6,7,8},
		{9,10,11,12}
	};
//此时我们剖析一下int arr[row][column]
//在实际内存中,存储了row*column个元素的地址
//而在通过row访问的是一维数组中的首地址,其元素地址一般来说是连续的
//内存表现:
// \start:	
//	&int [0][0] &int [0][1] &int [0][2] &int [0][3] \ 
//	&int [1][0] &int [1][1] &int [1][2] &int [1][3] \
//	&int [2][0] &int [2][1] &int [2][2] &int [2][3] \ 
//	&int [3][0] &int [3][1] &int [3][2] &int [3][3] \end 


//二维数组作形参,需要指明其有列数个指针指向类型
//int (*arr)[column] 表示有column个指针指向int数据
int sum_arr(int(*arr)[column], int row) {
	int total = 0;
	for (int r = 0; r < row;++r){
		for (int c = 0; c < column; ++c) {
			total += arr[r][c];
		}
	}
	return total;
}
//当二维动态数组时,则作形参传递的是双重指针。

结构体作形参

第一种是按照传值的方式

第二种是利用指针传递地址

struct a{};
void example(a* a1);//此处使用指针传递地址方式,在后续的使用地址对应变量的内容时,需要使用*
example(&a);

第三种是利用引用传递(也可修改结构体成员变量值)

typedef struct a{
    int num;
    int id
}*pt;
//引用传递,类似于给原本的实参,创建了一个别名,在别名上的修改等于原本变量上的修改
//具体对成员的操作符还需看形参的类型
void example(a &a1);//此时因为形参类型为a,对结构体对象a1的成员操作用“.":a.id
void example(pt &a1);//此时因为形参类型为*pt(指针),对结构体对象a1的成员操作用"->"a->id
example(a);

递归函数

通过调用自身,求得目标值,但需要注意的是,其调用自身时,因为变量作用域问题,其实每个嵌套函数都是单个存储内存中,有对应的值。

>>
Counting down ... 4 (n at 0012FEOC) 
Counting down ... 3 (n at 0012FD34) 
Counting down ... 2 (n at 0012FC5C) 
Counting down ... 1 (n at 0012FB94) 
Counting down ... O (n at 0012FAAC)
0: Kaboom! (n at 0012FAAC) 
1: Kaboom! (n at 0012FB94) 
2: Kaboom! (n at 0012FC5C) 
3: Kaboom! (n at 0012FD34)
4: Kaboom! (n at 0012FEOC) 

所以说函数的递归调用是指数级增长:$x(函数中调用自身个数)^n$

函数指针(重要

函数指针声明

double pam(int);//函数
double (*pf)(int);//函数作参数,*pf=pam pf为pam函数的指针。
double function_ptr_example(int *arr, double (*pf)(int));

函数指针进阶

数组作函数形参时的声明:

但在函数定义时,是必须要标注出参数类型参数名

const double * f1 (const double arr[],int n);
==
const double * f2 (const double [],int);//此处的声明只是省略了参数名
==
const double * f3 (const double *,int);//此处的声明是因为数组的传递是以指针的方式
const double * (*pam) (const double *,int) = f1;//此时的pam为函数指针,*pam=f1

声明一个函数指针数组,其元素为函数指针

const double* (*pa[3])(const double* arr,int size)={f1,f2,f3};
//为什么[]紧跟在*pa后面,因为[]的优先级比*高,先表明了有三个元素的数组
//此时看*pa[3]为一体,可看出是指针数组
//操作
const double * px = pa[0](av,3);//因为pa为函数指针数组的首地址且返回值为const double*类型
//如果想返回double值
double x = *(pa[0])(av,3)

函数指针数组的指针

const double* (*(*pa)[3])(const double* arr,int size)=&pa;
//我们只需要看*(*pa)[3]:先看(*pa),表明了pa是一个指针,而*(*pa)[3]表明了pa是指向的是有三个函数指针元素的数组。
==
auto = pa;

(*pa)[i];//*pa为函数指针数组的元素,也就是函数指针
*(*pa)[i];//表示的是函数指针返回值,也就是指针指向的值。

注意

//pa和&pa的区别,虽然从数字上来说,他们值相同。
//但是&pa表示整个数组,如果&pa+1,表示的pa这个数组后一块typeof(double)*元素个数的字节内存块的地址
//而pa表示的是&(pa[0])也就是首元素的地址

* *&pa == *pa == pa[0];//此处是想获取首元素的值

使用typedef进行简化

typedef const double* (*p_fun)(const double* arr,int size);//p_fun为函数指针类型的别名
//将各变量名放入p_fun中即可。
p_fun p1 = f1;
p_fun pa[3] = {f1,f2,f3};    
p_fun (*pa)[3] = &pa;

实例

函数返回值不是指针

//声明
int ch_1(const char* c);
int judge(int (*fp)(const char* c),const char* c);

int main(){	
	//以下函数指针都是声明 和 初始化一起
	int (*fp1)(const char*) = ch_1;//函数指针
	int(*fp2[3])(const char*) = {ch_1 ,ch_1 ,ch_1};//函数指针数组
	int(*(*fp3)[3])(const char*) = &fp2;//指针指向函数指针数组
    
    //*fp = ch_1
    *fp1("c"); //return int
    // int *a = &ch_1;fp2[0] =&a; (*fp2[0])  = ch_1;
    (*fp2[0])("c"); //return int
    //int *fp3 =&fp2; int *a = &ch_1;
    (*(*fp3)[0])("c"); //return int 
   }
//定义
int ch_1(const char* c){
    return (int)c;
}
int judge(int (*fp)(const char* c),const char* c){
    return fp(c);
}

函数返回值是指针

//声明
int* ch_1(const char* c);
int* judge(int* (*fp)(const char* c),const char* c);

int a=0;//因为后续需要使用全局返回ascii编码值
int main(){
	char ch[2]="12";
	//以下函数指针都是声明 和 初始化一起
	//函数指针 *fp1 = ch_1
	int* (*fp1) (const char*) = ch_1;
    
    //此处的指针数组可能有点难以理解,也就是说fp2是一个三个int类指针元素的数组
    
	//函数指针数组 *fp2[3] = {ch_1,ch_1,ch_1}
	int* (*fp2[3]) (const char*) = {ch_1 ,ch_1 ,ch_1};
	//指针指向函数指针数组 *(*fp3)[3] = &fp2;
	int* (*(*fp3)[3]) (const char*) = &fp2;
	
    
    //all return int;
    //*fp1 = ch_1, *(*fp1)(ch) = *ch_1(ch)
	cout << "fp_1:" << *(*fp1)(ch) << endl;
    //*fp2[0] = &ch_1, *(*fp2[0])(ch) = * ch_1(ch)
	cout << "fp_2:" << *(*fp2[0])(ch) << endl;
    //*fp3 = &fp2, *(*fp3)[0] = &ch_1, *(*(*fp3)[0])(ch) = * ch_1(ch)
	cout << "fp_3:" <<  *(*(*fp3)[0])(ch) << endl;
	
	cout << judge(fp3)
	
   }
//定义
int* ch_1(const char* c){
    a = (int)c[0];
    return &a;
}
int* judge(int* (*fp)(const char* c),const char* c){
    return fp(c);
}

内联函数

声明和定义都需要加上inline关键字。

内联函数与普通的函数调用不同的是,在编译时,遇到内联函数调用就先把它的定义用来替换调用位置,然后传入对应参数。就不需要像普通函数调用,需要寻找函数的定义位置,再传入参数。

//编译前
inline double sum_arr(double * arr_head,double* arr_rear);//声明内联函数

int main(){
const int size = 3;
double arr[size] = {1.1,2.2,3.3};
double  sum = sum_arr(arr,arr+3);
}

inline double sum_arr(double * arr_head,double* arr_rear)//内联函数定义
{	double sum=0.0;
 for(;arr_head != arr_raer;++arr_head){
     sum += *arr_head;
 }   
 return sum;
}

//编译后,源代码发生变化:内联函数调用处被替换
inline double sum_arr(double * arr_head,double* arr_rear);//声明内联函数

int main(){
const int size = 3;
double arr[size] = {1.1,2.2,3.3};
double  sum = {//此处的内联函数调用被替换成函数定义
 	double sum=0.0;
 	for(;arr != arr+3;++arr){
     	sum += *arr;
 	}   
 	return sum;;
}

inline double sum_arr(double * arr_head,double* arr_rear)//内联函数定义
{	double sum=0.0;
 for(;arr_head != arr_raer;++arr_head){
     sum += *arr_head;
 }   
 return sum;
}

其实定义可以直接放在函数调用上一层的作用域,当然要该函数层数不多的情况下。

当内联函数的语句块过于复杂时,是不建议使用内联函数的

C宏定义和内联函数的区别

宏定义中的单个#的功能类似于文本替换。

#define sum(arr) # arr+arr
int main(){
   int a =  sum(2);// instead : int a = 2+2
}

不适合于传递复杂数据类型;

不能按值传递,也就是当我传递一个2+3作为参数时,它是不会把2+3计算的结果传递给宏的。

当传递右自增自减(c++,c--)时,宏会进行两次,而内联函数只会进行一次(先计算结果再进行自增和自减)。

引用变量

引用变量主要用于处理结构和类对象。

当有一个派生类,通过引用基类,可以指向该派生类。

左值引用的普通声明

从变量的声明看,我的理解就是给某一个变量起了一个别名。

int red = 1;
int & a = red;//此处表示a是int类型变量red的引用
//需要明白的是,此刻的变量a和red指向相同的值和内存单元(地址)

注意:引用在声明时就需要初始化

引用变量更类似与指针常量

int* const a = &red//声明即初始化,且不能修改地址,只能修改其引用值

如果想通过指针保存变量地址,然后引用指针,来修改引用变量,是不可能行。

int a = 20, b = 50;
int* pt = &a;
int &rodents = *pt;
pt = &b;//此操作不可行

函数形参的左值引用变量

非const引用变量做形参,其实参一定是传入了 变量,而非常量和表达式(也就是非左值)。

左值

变量、数组元素、结构成员、引用、解除引用的指针

非左值

常量(除字符串常量)、表达式

const左值引用变量

在形参的引用变量前加上const关键字,使其在某些情况下产生临时变量,避免因为部分原因修改引用的变量的值。

情况如下(部分原因:

  1. 实参的类型正确,但不是左值;(2.00为double常量)

  2. 实参的类型不正确,但是可以转换为正确的类型。(int a=3;而double &rd = a;a为int可隐式转double)

这个时候都会生成匿名的临时变量,并在函数调用后销毁而不会修改传入的实参值。

double side = 5;
double *pd = &side;
double &rd = side;
double lens[4] = {1.1,2.2,3.3,4.5};
//以上做形参都可以
long edge =5L;
sum(side +5);
sum(7.0);
//以上做形参会产生临时变量
double sum(const double& a){
return a += a;
}

所以,建议多使用const:

  1. 使用const可以避免无意中修改数据的编程错误;

  2. 能够处理const和非const实参;

  3. 能够正确生成并使用临时变量。

在编写代码上

习惯将不修改值的引用形参加const;

若是想原地修改实参值,则返回对应的引用形参即可(提高效率,不需要像普通的需要拷贝值或者结构);

若返回引用形参不能做左值,则在返回值前加个const关键字。

右值引用

顾名思义,就是指向右值的引用(&&)。

为什么部分函数返回值为右值,因为该返回值是属于函数的匿名临时变量,调用完成后将被删除,也就调用完毕后,返回值没有在内存单元中。

引用类对象

类对象:(string、ostream、istream、ofstream、ifstream)等类的对象作为函数的形参

C-风格字符串char *可转换为C++string类对象。

在函数形参引用为const string &,而实参传递"***"为字符串常量(或则C-风格的字符串),由于形参是const类型的引用,依据上一小节的内容,会创建一个匿名临时变量,将C风格字符串转换为const string类型,然后再引用给形参。需要记牢const修饰的形参会起到作用。

何时使用值传递、指针和引用

值传递:

当传递的数据对象很小:如内置数据类型或小型结构

在使用指针和引用时,其数据对象应是较大,且程序员想进行修改的情况下,这样的操作能够提高运行速度。

指针传递:

当传递的是数组(只能使用指针、结构

引用传递

当传递的是结构(struct类型)、类对象:string、ostream(只能使用const 引用)

默认参数

在函数声明的同事设置默认值,函数定义不需要设置。当函数调用时,已设置默认值的参数没有传入实参,则将默认值传递到函数体中。否则有传入实参时,将覆盖默认值传递。

要为某个参数设置默认值,则它的右边参数也要有默认值。

C++函数多态

  1. 函数名相同,函数参数不同(可以是数目不同,类型不同);

  2. 函数是无法区分仅以返回值不同,也就是还需要参数不同。

//overloaded
int* ch_1(const char* c) {
	a = (int)c[0];
	return &a;
}

string* ch_1(string* str) {
	
	return str;
}
char ch[2]="1";
ch_1(ch);//被允许的,ch可转换为const类型

double ch_2(double n) {
	return n;
}
int ch_2(double n) {//error 函数无法区分仅以返回值不同
	return n;
}

需要注意的是:函数的重载需要从编译器的角度考虑,因为有些参数类型的改变其实本质是一致的。那这样的函数重载是不被允许的。

非const类型转const类型是允许的

重载引用

//左值引用
void sink(double& r1) {//#1
	std::cout << r1 << std::endl;
}
void sink(const double& r2) {//#2
	std::cout << r2 << std::endl;
}
//右值引用
void sink(double&& r3) {//#3
	std::cout << r3 << std::endl;
}
double x = 5.5;
const double  x1 = 6.6;
//调用函数如下
sink(x);//#1
sink(x1);//#2
sink(x+x1);//#3

有意思时,当函数重载引用参数类型丰富时,调用该函数,会调用参数最为匹配的重载函数。

何时使用函数重载

当函数执行任务相同,但需要处理不同参数的数据时使用函数重载。

名称修饰,用于根据函数原型中指定的形参类型对每个函数名进行加密

函数模板

函数模板只是告诉编译器,应如何创建函数,是生成函数定义的方案。只有在使用到时才会生成对应参数的函数实例。(是隐式实例化:告诉编译器函数参数类型,才生成函数实例

常见情况下:函数模板被放在头文件下

//typename和class创建模板参数类型的意义是等价的
template <typename[class] anyType> //先声明要创建一个模板,且类型命名为anyType
void swap(anyType& a, anyType& b){
    //再详细描写函数头,返回值,函数名,参数列表(其中的模板参数类型为anyType)
    anyType temp = a;
    a = b;
    b = temp;
}

实例:

#include<iostream>
#include<string>
using namespace std;
template<typename T> void Swap(T&, T&);
template<typename T> void show_cout(T&);
int main(){
	string str1 = "123";
	string str2 = "456";
	Swap(str1, str2);
	show_cout(str1);
	show_cout(str2);
}
template<typename T>
void Swap(T& a, T& b) {
	T temp;
	temp = a;
	a = b;
	b = temp;
}
template<typename T>
void show_cout(T& a) {
	std::cout << a << std::endl;
}

函数模板的重载

函数模板也可以重载,只要其参数数目和类型不同,或者返回值和参数的不同。

template<typename T> void Swap(T&, T&);
template<typename T> void Swap(T&, T&,int);

template<typename T> void show_cout(T&);
template<typename T> void show_cout(T *,int);
//Swap : overloaded
template<typename T>
void Swap(T& a, T& b) {
	T temp;
	temp = a;
	a = b;
	b = temp;
}
template<typename T>
void Swap(T& a, T& b, int size) {
	for (int i = 0; i < size; ++i) {
		int temp = a[i];
		a[i] = b[i];
		b[i] = temp;
	}
}
//show_cout: overloaded
template<typename T>
void show_cout(T& a) {
	std::cout << a << std::endl;
}


template<typename T>
void show_cout(T *a,int size) {
	for (int* i = a; i != a+size; ++i) {
		std::cout << *i <<" ";
	}
	cout <<std::endl;
}

模板的局限性

当模板的参数为数组、指针和结构时,是需要特定语句体操作的,需要防止其他重载的模板函数被误传入参数。而出现错误。

显式具体化

不同函数模板,隐式的通过传参告诉编译器生成的函数实例。

具体化显式的直接告诉编译器特点的参数类型,创建特定的实例(比如为形参某结构体)。

当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

第三代具体化(C++98标准)

  1. 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。

  2. 显式具体化的原型和定义应以 template<>打头,并通过名称来指出类型。

  3. 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

void Swap<job> (job &,job &);//非模板函数
template <typename T>void Swap( T& , T& )//模板函数
template <> void Swap<job> (job &,job &);//具体化 函数名后的job可以省略
struct job{ 
	char name[40];
	double salary;
	int floor;
}

template <> void Swap<job> (job &j1, job &j2){
    
}

显/隐式实例化和显式具体化

显/隐式实例化和显式具体化都被称为具体化,因为是使用具体的函数定义。

显/隐式实例化:

编译器被传参告知,模板形参类型,使用模板生成函数实例

//隐式实例化
template <typename T> void Swap(T& ,T&);
//显式实例化
template  void Swap<int >(int & ,int &);//在函数调用使用

显式具体化:

编译器直接就可读取到的形参类型对应的函数。告诉编译器不需要使用模板来生成函数定义

template <> void Swap<st> (st&, st&);

显式实例化和显式具体化的区别:

  1. 是否使用模板生成;

  2. 以及在template关键字后是否跟"<>".

//隐式实例化,也就是常见的模板
template <typename T> void Swap(T& ,T&);
//显式实例化,也就是隐式实例化指明了类型,道理相同,只不过是为了找错时更好找
template  void Swap<int >(int & ,int &);
//显式具体化,在编译器匹配可行函数的优先级高于模板实例化
template <> void Swap<st> (st&, st&);

int main(){

    int a=1,b=2;
    Swap(a,b);//此刻将调用模板的显式实例化
    
    st st1,st2;
    ...//初始化
    Swap(st1, st2);//此刻将调用显式具体化
    
    double d1 =1.2, d2=3.2;
    Swap(d1,d2);//此刻将调用模板的隐式实例化
}
//在main中直接告诉编译器调用什么版本的函数
...//先在main外部定义了函数function()
//函数调用
function<>(实参);//告诉编译器调用function重载函数的隐式实例化
function<int>(实参);//告诉编译器调用重载函数的显式实例化,且模板参数类型为int

编译器如何选择哪个函数版本

编译器选择函数版本步骤如下:

第一步

创建候选函数列表,其中与调用函数同名的非模板函数和模板函数。

第二步

使用候选函数创建可行函数列表,其中包括实参传入形参(可隐式转换的函数),或者使用模板生成一个实例。

第三步

确定最佳可行函数,如果有,则使用,否则,则返回函数调用错误。

确定函数为最佳可行函数的评判顺序

  1. 完全匹配,但常规函数优先级高于模板(包括显式具体化)
  2. 提升转换(例如:char和short自动转换为int,float自动转换为double)
  3. 标准转换(例如:int转换为char,long转换为double)
  4. 用户定义的转换,如类声明中定义的转换

完全匹配和最佳匹配

在进行完全匹配时,C++允许某些“转换”:

image-20230207193247681

当有多个匹配的原型时,编译器是无法完成重载解析过程,并返回如:“ambiguous(二义性)"单词的句子

const和非const之间的区别只适用于指针和引用

当形参为指向非const数据的指针和引用时,它优先与非const指针和引用实参匹配。

部分排序规则(C++98)

实参传入形参,其隐式转换较少的一个模板优先于其他重载模板。

template<typename T>
void showarray(T arr[], int n);//template A

template<typename T>
void showarray(T* arr[], int n);//template B
struct test_data{
    char name[50];
    double amount;
}
int main(){
    int arr1[3]={1,2,3};
    test_data td [3]={
        {"joy",2400.00},
        {"marin",3200.00},
        {"kevin",4100.00}
    };
    
    double *arr2[3];//指针数组
    for(int i =0;i<3;++i){
        arr2[i] = &td[i].amount;//将结构体对象数组对应的amount成员地址赋予arr2
    }
    showarray{arr1[],3};//template A: typename =int
    
    showarray(arr2[],3);//template B: typenam ==double *,因为arr2为指针数组,模板B的转换更少,所以选择它
    
}

总结

编译器选择函数版本时,会先考虑函数列表,将所有可用函数找到后,再根据参数匹配度(实参传入形参是否转换,以及转换的步骤是否较少)选择可行函数。

其函数版本优先级:(高到低,从左往右

常规函数、显式具体化、显示实例化、隐式实例化。

补充:(C++11提供)

当模板的参数类型不同时,局部变量类型不知道时:

template<class T1, class T2> void Sum(T1 x, T2  y)
{
	?type? xpy = x + y;//xpy不知道声明为什么类型
    //C++11做出的修改,使用decltpye(expression)
    decltype(x+y) xpy = x + y;
}

decltype(expression) var返回给声明var的类型

  1. 如果expression没有用一个括号括起来,则var的类型与该标识符的类型一致,包括const等限定符。
double a = 4.4;
const double & pd = a;
decltype(pd) r1 = a;//r1 is const double &
  1. 如果expression为函数调用,则var与函数的返回值类型相同。
long inneed(int);
decltype(inneed(2)) m;//m is long
  1. 如果expression是一个左值,那么var将会是一个引用类型。当expression被括起来时,var也是一个引用类型
double xx = 4.4;
decltype(xx) w= xx;//w is double
decltype((xx)) r2 = xx;//r2 is double &
  1. 如果以上情况都没用,那么var的值与expression(表达式结果)里的一致。
int j =3;
decltype(j+3) s; //s is int
decltype(5L) d; //d is long

当无法预知返回值的类型时:(后置返回类型)

template<class T1, class T2> auto Sum(T1 x, T2  y) ->decltype(x + y)
{
	return x + y;
}

文件

使用头文件<fstream>

写入文件

就相当于把std::cout换成ofstream类型的文件对象。

#inclde<fstream>
using namespace std;

//创建文件操作对象
ofstream 变量名;
ofstream OutFile;
变量名.open("Filepath");//打开文件
OutFile.open("..\\..\\test.txt",ios::ate|ios::out|ios::in)
变量名 << 操作数据变量; //写入
char a[20] = "Hello World!";
OutFile <<a ;
OutFile.close()\\关闭文件

读取文件

#inclde<fstream>
#include<string>
using namespace std;
//创建文件操作对象
ifstream 变量名;
ifstream Readfile;
string str;
Readfile >> str;

open(const char* szFileName,int mode)

szFileName为被操作文件路径。

int mode:文件打开模式如下:

模式标记 适用对象 作用
ios::in ifstream fstream 打开文件用于读取数据。如果文件不存在,则打开出错。
ios::out ofstream fstream 打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。
ios::app ofstream fstream 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。
ios::ate ifstream 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。
ios:: trunc ofstream 打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。
ios::binary ifstream ofstream fstream 以二进制方式打开文件。若不指定此模式,则以文本模式打开。
ios::in | ios::out fstream 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out ofstream 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out | ios::trunc fstream 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。
ios::ate | ios :: in | ios::out ofstream 打开文件,且指针移向文件内容末尾,并可读写数据

文件对象.is_open()

用于判断文件是否正常打开,若文件没被打开则返回false。

实际例子

读取一个数据为数字的文件,将其所有数字相加并取平均值。

ifstream inFile;
char  FileName[50];
cout << "Enter you want operate file_path:";
cin >> FileName;
inFile.open(FileName, ios::in | ios::out);

//is_open()用于文件是否被正常打开(正常来说读取文件时要有)
if (!inFile.is_open()) {
	cout << "Oh!E,you file couldn't open ." << endl;
	exit(EXIT_FAILURE);
}

double value = 0.0, sum = 0.0;
int count = 0;

inFile >> value;
//good()函数用于判断当前数据流是否正常(正常来说读取文件时要有)
while (inFile.good()) {
	++count;
	sum += value;
	inFile >> value;
}
//该读取方法还可以这样:
//    while(inFile>>value){
//       ++count;
//        sum += value;
//    }

//eof()用于检测循环读取文件内容是否正常结束(正常来说读取文件时要有)
if (inFile.eof()) {
	cout << "End of file reached!" << endl;
	}
else if (inFile.fail()) {//fail()用检测类型不匹配
	cout << "Input terminated by data mismatch!" << endl;
}
else {//剩下的是未知情况
	cout << "Input terminated for unkown raeson!" << endl;
}

if (count == 0) {
	cout << "No data processed.\n";
}
else {
	cout << "Items raed:" << count << endl;
	cout << "Sum: " << sum << endl;
	cout << "Average:" << sum / count << endl;
}
inFile.close();

将ostream作为形参

void file_it(ostream& os , double fo, const double fe[],int n){
	//ios_base类是输入输出流的一个基础类
	//fmtflag用于存储格式标记	
    ios_base::fmtflags initial;
	initial = os.setf(ios_base::fixed);//因为控制台在显示大一点的数使用的是科学计数法,fixed告诉控制台使用一般方式显示
	os.precision(0);
	os << "Focal length of objective: " << fo << "mm\n";
	os.setf(ios::showpoint);//设置输出变量将显示小数部分
	os.precision(1);//控制浮点数输出的小数位数
	os.width(12);//控制输出宽度
	os << "f.1. eyepiece";
	os.width(15);
	os << "magnification" << endl;
	for(int i =0 ;i < n;++i){
		os.width(12);
		os << fe[i];
		os.width(15);
		os<< int (fo/fe[i]+0.5) << endl;
	}
	os.setf(initial);//初始化之前的输出格式设置
}

系统

把类型创建别名

//有两种方法
//First method:使用预处理器阶段
#define 别名 类型
//Second method:使用c++方式,将已有类型建立一个新名称(更建议使用这个方法)
typedef 类型 别名

执行等待

使用clock()和ctime库。

CLOCK_PER_SEC,获取当前秒为单位量。

输入输出流(重点)

cin(要明白输入流如何正确作用于对象)

是一个封装的类对象,其作用是将用户输入字符插入输入流中,并以空格、制表符和换行符来确定结束位置。

在输入字符串对字符数组进行赋值的时候,建议使用cin.getline(arrayname,lens)或cin.get(arrayname[,lens])

当使用cin作条件运算时,它会自动进行bool值转换此方法常用捕获异常。

例如:

int x = 0;
//也就是说当我cin输入后,不是捕获变量的数据类型,则会返回false。
if (cin>>x) cout <<"recent x is number";
else cout << "recent x is not number";

输入一系列数字的例子

	int x[5];
	for (int i = 0; i < 5; ++i) {
		while (!(cin>>x[i]))//当输入为非数字时,则需要进行输入流处理,此时非数字的字符流在输入流中
		{	
			cin.clear();//第一步:先清除输入流
			while (cin.get() != '\n') {//第二步:输入流是否还有其他字符,若有则继续清除。
				continue;
			}
		}
	}

cin.getline(ch,size)

读次取每一行,通过换行符确定行尾。但是使用空字符替换换行符来存储。

cin.get()

它和getline不同的是,不会丢弃换行符,会保留于输入流中。

若要连续读取行,可使用cin.get()无参形式,这样可获取换行符并丢弃,下一个带参get可正常获取输入流内容。

当cin.get(arrayname,lens),则会在输入流中选取lens个字符存入array数组中,若没有get函数没有lens参数,则默认单个字符存放其中。

//当输入错值,需要进行丢弃并跳出输入
double value = 0.0;
if(!cin>> value){//when input data is mismatch,输入类型不匹配
    while(cin.get() !=  '\n'){//丢弃该输入数据
        continue;
    }
    break;
}

拓展:

一般调用cin的函数返回一个cin对象,所以可以在其重复操作

cin>>variable;//此句形式返回的是cin对象;
 //所以可以复合使用:
(cin>>variable).get();//来处理换行符,使后续的get能正常读取
cin.get(ch1).get(ch2)//cin.get(ch1)放回一个cin对象,用于调取get(ch2).

看输入方面 cin>>ch 与cin.get(ch)和 ch=cin.get( )有什么不同?

cin>>ch不会获取空字符、换行符和制表符,而后两者都会获取这三个字符。

cin.rdstate()查看错误状态

错误对应等级
0 :goodbit 无错误 
1 :Eofbit 已到达文件尾 
2:failbit 非致命的输入/输出错误,可挽回 
3:badbit 致命的输入/输出错误,无法挽回 

cin.sync()清除缓冲区数据

cout

其作用是将程序员指定内容插入输出流中,并显示出来。

输出格式

fix
cout << fixed;
//用一般的方式输出浮点型,例如C++程序在控制台显示的时候大一点的数,显示的时候使用了科学计数法,使用该命令即可像一般的方式显示
precision
cout.precision(2) ;
//设置精确度为2,并返回上一次的设置。
setf
cout.setf(iOS_base::showpoint);
//显示浮点数小数点后面的零。

文件尾条件

(也就是文件类输入时,结束时的条件符号EOF:"ctrl+z")

使用cin.fail()函数来检测输入流是否有文件尾EOF符号,需要注意的是: cin.fail()函数用于读取输入流的结果,应放在输入流操作符之后

cin.get();
cin.faill() == false;//cin.fail()函数,检测到EOf符号返回1
char ch;
cin.get(ch)
while( !cin.fail()){
cout << ch;
}
==
int ch;
ch = cin.get();
while ( ch != EOF){
cout.put((char)ch);
}
//在c语言中getchar()参数char类型,返回int型数据。
//在c语言中cout.put()参数char类型

需要知道的是。EOF 不表示输入中的字符.而是指出没有字符,即不是有效字符

输入流重置

cin.clear()使用该函数重置输入流。

编译器代码执行顺序

副作用:指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改。

顺序点:是程序执行过程中的一个点,在c++中语句中的分号就是一个顺序点。在执行下一条语句前需要赋值、递增/减的运算符执行完毕。

int x = 1;
cout << (4 + x++) + (1 + x++);//该语句执行完毕前的值为7,执行完毕后x为3;

逗号运算符

其逗号运算符中的语句同级,也就是说会同时执行完这两个语句才结束这一步;

int a, b == int a ,int b;
int y = (1,024); //此处的y会赋值先赋值为1,然后在赋值为八进制的024;
int x;
x = 1,024;//此处则会出现x=1,而024没有变量被赋值则丢弃。

内存管理

先看下图:

ps:该图引用某博主的技术贴的照片

  1. 栈又叫堆栈 -- 非静态局部变量 / 函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
    创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段-- 可执行的代码 / 只读常量。

也就是局部变量和函数的参数都存放在栈中;而用户动态开辟的空间会使用堆空间;全局变量和静态变量都在数据段中。

new和delete

先看源代码:

// operator new:
// 该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;
// 申请空间失败尝试执行空间不足应对措施,如果用户设置了应对措施,则继续申请,否则抛异常。
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}
 
// operator delete:
// 该函数最终是通过free来释放空间的
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK);  // block other threads
	__TRY
		// get a pointer to memory block header
		pHead = pHdr(pUserData);
	// verify block type
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg(pUserData, pHead->nBlockUse);  //此处调用free函数
	__FINALLY
		_munlock(_HEAP_LOCK);  // release other threads
	__END_TRY_FINALLY
		return;
}
 
// free的实现
#define  free(p)        _free_dbg(p, _NORMAL_BLOCK)

从源代码可知,new和delete关键字的本质,也是使用malloc和free函数,来申请空间和释放空间。

在关键字后面加上“[]”,表示是连续的区域。

new和delete的运行过程

在new的时候,会在堆区申请(连续)空间,并调用构造函数。在delete时,会先调用析构函数,再释放堆中的(连续)空间.

但相比较而言,其区别如下:

  1. new关键字会调用构造函数functionName(),而delete关键字会调用析构函数~functionName();而malloc和free函数不会调用。
  2. new关键字在申请空间失败时,会返回异常,需要进行异常处理;而malloc函数会返回一个NULL。
  3. new关键字只需要表明申请空间存放的对象类型即可new int[]{};而malloc函数在申请空间时需要手动计算空间大小并传递void* ,需要进行强转;

内存泄漏

指因为疏忽导致程序未能将不再使用的内存空间及时释放,导致的内存浪费和无法控制该段空间。

堆内存泄漏

指在动态申请空间new或malloc时,没有及时delete和free释放空间导致。

系统资源泄露

指程序使用系统分配的资源,比如方套接字、文件描述符、管道等没有使用对应函数释放,导致系统资源持续占用的浪费。

避免内存泄露的建议

  1. 养成良好习惯,申请空间,就要释放空间。
  2. 使用智能指针进行申请空间
  3. 采用RAII思想

在c++中分了三种存储变量形式

自动存储

其存放的是函数内部定义的常规变量。存放于栈中,执行先进后出的规则。

静态存储

其存储的是程序执行期间都存储在的变量。例如:static,主函数外定义的变量。

动态存储

使用new或者malloc()生成的变量。需要注意及时释放delete或free();

错误异常

cstdlib头文件

exit(error_info)用于系统抛出错误后,退出并输出error_info。

访问修饰符

public(公共的)

对于被修饰的数据类型,可以被该类和非该类访问。

private(私有的)

对于被修饰的数据类型,除了该类,无法被其他类访问。

protected(保护的)

对于被修饰的数据类型,除了该类和该类的继承类,无法被其他类访问。

设计模式

聚合

为动态关系,即部分和整体的生命周期未必一致。

组合

为静态关系,即部分和整体的共存亡。

本笔记属于自用,有部分内容是笔者觉得没必要再叙述,需要如此。且有照片引用部分内容属于搬运其他博主内容。

标签:const,函数,记录,int,double,C++,学习,类型,指针
From: https://www.cnblogs.com/duuuuu17/p/17219616.html

相关文章

  • 爬虫学习02之模拟登录以及代理
    一、调试模式介绍调试模式,即进入网页页面半代码模式,查看网页与代码一一对应关系。鼠标右键,再出现选项中找到检查进入调试模式,或者按键盘上的F12键进入调试模式。功能介......
  • salesforce零基础学习(一百二十七)Custom Metadata Type 篇二
    本篇参考: salesforce零基础学习(一百一十一)custommetadatatype数据获取方式更新https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_metho......
  • python数据库学习笔记
    1.数据库管理软件的本质?本质就是C/S架构的套接字程序服务器套接字操作系统:linux计算机(本地文件)2.为什么要用数据库管理软件?可以自己写,要结合套接字程序,解决并发......
  • 案例分析:控制文件序列号满故障处理记录
    记一次控制文件序列号满的分析处理过程,聊聊我的思路。技术人人都可以磨炼,但处理问题的思路和角度各有不同,希望这篇文章可以抛砖引玉。以一个例子为切入点一、问题背景基......
  • 【学习日志】Java基本数据类型的自动装箱和拆箱
    //测试代码publicstaticvoidmain(String[]args){Integera=1;Integerb=2;Integerc=3;Integerd=3;Integ......
  • ScanNet DataSet数据集下载记录
    事情是这样的。因为最近在研究VisualGrounding的工作,但是3d领域的VG的数据集到笔者目前写这篇文章为止只有三个,即Scanrefer、Nr3D和Sr3D,他们都是基于ScanNet数据集的。而......
  • C++ 构造函数和析构函数
    构造函数和析构函数目录页面问题构造函数与析构函数初始化列表转换构造拷贝构造(这种都是浅拷贝,每一项成员依次拷贝过去)默认的赋值运算符小的总结页面构造/......
  • java接口学习笔记
    目录java接口学习笔记1.抽象类和抽象方法2.接口3.抽象类和接口对比参考java接口学习笔记1.抽象类和抽象方法抽象方法:abstractvoidf();抽象类:包含抽象方法的类称......
  • C++ 常用语法
    1.定义一个字符串常量staticconststd::stringversion("0.0.1");staticconststd::stringname("Car-"+version);2.定义size大小staticconstexpruint64_tsh......
  • 网络流学习笔记
    题单+1网络流一个有向图,每条边描述一个有向的限制条件,如果我们把它类比为水流的话,每条边就维护一个最大的水流量。一般有一个源点(可以流出无限多的水)和一个汇点(所有......