第二章—C++基本语言

目录

二、命令空间简介、基本输入输出

1、命名空间

2、基本输入输出cin、cout精解 

三、auto、头文件防卫、引用、常量

(1)局部变量及初始化

(2)auto:变量的自动类型推断

(3)头文件防卫式声明

(4)常量(const与constexpr)

const

 constexpr

四、范围for,new内存动态分配,nullptr

(1)范围for

(2)new内存动态分配

 (3)nullptr(真正的指针类型)

五、结构,权限修饰符,类简介

(1)结构

(2)权限修饰符

(3)类简介

六、函数新特性,内联函数,const详解

(1)函数回顾与后置返回类型、auto与decltype

返回值类型后置初见

auto与decltype详细解析

 返回值类型后置解决的问题

总结 

(2)内联函数

 (3)函数杂合用法总结

 (4)const char*、char const*、char* const 三者区别

七、string类型介绍

(1)简介

(2)定义和初始化string对象

(3)string对象上的操作

(4)读写string对象

(5)范围for针对string的使用

八、vector类型介绍

(1)简介

(2)定义和初始化vector对象

(3)vector对象上的操作

(4)范围for的应用

 九、迭代器精彩演绎,失效分析及弥补,实战

(1)迭代器简介

(2)容器的迭代器类型

(3)迭代器begin()/end()操作,反向迭代器rbegin()/rend()操作

 (4)迭代器运算符

(5)const_iterator常量迭代器

(6)cbegin()和cend()操作

(7)迭代器失效

(8)灾难程序演示

十、类型转换:static_cast,reinterpret_cast等;静态断言

类型转换

(1)static_cast:静态转换

(2)dynamic_cast:动态类型转换,运行时类型识别和检查。

(3)const_cast:去除 指针或引用 的常量属性(不是指针、引用无法转变)

(4)reinterpret_cast:重新解释类型

静态断言


二、命令空间简介、基本输入输出

1、命名空间

  • (1)简介

同名实体定义:同名函数、同名变量、同名的类定义;
命名空间就是为了防止名字冲突而引入的一种机制。系统中可以定义多个命名空间,每个命名空间都有自己的名字,不可以同名;
可以把命名空间看成一个作用域。在这个命名空间定义函数,和在另一个命名空间定义的函数,即使同名,也互不影响;

  • (2)定义
namespace 命名空间名
{

}

命名空间的定义可以不连续,甚至可以写在多个.cpp文件中,如果以往没有定义这个命名空间,那么 namespace 命名空间名 就相当于定义一个命名空间;如果以往定义了命名空间,那就相当于打开已经存在的命名空间并为其添加新成员的声明。

  • (3)外界如何访问命名空间中的变量、函数

命名空间名::实体名。
其中,::叫作用域运算符 

  • (4)使用命名空间

using namespace 命名空间名
这样以后使用命名空间中的实体就不必再加 (命名空间名::)了 。

2、基本输入输出cin、cout精解 

  •  (1)iostream库:输出输出流

流:就是一个字符序列
cout:console output,是个对象,"标准输出"对象,我们就认为这个对象是屏幕
<< :输出运算符,c++对其进行了重载,
<< :可以当成函数,有参数,例如:cout<<1。参数1在左边,就是cout对象;第二个参数就是 << 右边的实体。可以理解为将 参数2实体 送入参数1(即cout对象,屏幕)
endl:模版函数名,相当于函数指针
endl作用:换行、强制刷新输出缓冲区
输出缓冲区:一段内存,cout输出的时候是往输出缓冲区输出内容,那么输出缓冲区什么时候把内容输出到屏幕去的呢?

  • 1.缓冲区满了
  • 2.程序执行到main的return语句
  • 3.调用了endl,能够强制刷新输出缓冲区(把缓冲区的内容往屏幕上输出)
  • 4.当系统不太繁忙的时候,系统也会查看缓冲区内容,发现新内容也会正常输出到屏幕

<< 定义:ostream& std::cout.operator<<();

<< 返回的是一个写入了给定值(参数2)的cout对象。

  • (2)std::cin

cin:标准输入,"iostream"相关对象,可以就理解成键盘
>> :输入运算符。返回其左侧运算对象作为其计算结果
cin >> val1 >> val2; --> (cin>>val1)>>val2。

三、auto、头文件防卫、引用、常量

(1)局部变量及初始化

C++中,局部变量随时用到随时定义 。

int i={ 5 };     //定义变量的同时初始化变量(c++ 11 ),=可有可无
int a[]{ 1,2,3,4,5 }; //定义数组的同时初始化数组
好处:int a = 3.5f; //可以编译成功
	 int a{ 3.5f }; //无法编译成功,因为3.5f是浮点数,而a是整形

(2)auto:变量的自动类型推断

auto可以在声明变量时,根据变量初始值的类型自动为此变量选择匹配的类型(声明时要赋初值)。 auto自动类型推断发生在编译期间,所以使用auto不会造成程序执行效率降低。

例如:

auto val = true;//bool
auto ch = 'a'; //char

(3)头文件防卫式声明

防止头文件重复包含。

// 法1
#ifndef 标识符  (如果没有用define定义过标识符,则运行程序段)
#define 标识符
	 程序段
#endif

//法2
#pragma once

(4)常量(const与constexpr)

const

C语言中的const:

C语言中const修饰的变量是只读变量,在使用const关键字声明定义变量时会给该变量分配内存空间。不能通过变量名直接修改该变量的值,但是可以通过指针的方式修改该变量对应的值,从某种意义上来说,C中const修饰的变量不是真正意义上的常量,可以将其当作一种只读变量。

  • const修饰全局变量时,该变量存储在文字常量区(只读),不能通过指针的方式修改其内容。const修饰的全局变量默认是外部链接的,即其它源文件可以直接使用该变量(但是需要声明一下)。

  • const修饰局部变量,该变量存储在栈区中(可读可写),通过指针的方式可以修改其内容

//fun.c
const int global_a=100; //global_a的本质是只读的变量,存放在文字常量区(内存空间只读)


// test.c

// 对fun.c中的global_a进行声明(不要赋值)
extern const int global_a;
int main()
{
    const int a=10;
    int* point_a=&a;

    a=20;              //error
    global_a=200;      //error
    *point_a = 20;     //ok
    
    *point_a=&global_a;
    *point_a=200;    //error

    
    return 0;
}

C++中的const:

C++中 const修饰的变量不需要创建内存空间,当碰见常量声明时,C++不会单独的给
它分配内存中,而是将他分配在符号表中,符号表的存在形式是一个名字对,比如定义常量const int data = 10;,C++会在一张符号表中添加 namedatavalue为10的一条记录,如下图所示: 

  1. 既然,const修饰的变量没有内存空间,所以在C++中const修饰的变量才是真正意义上的常量。
  2. 编译过程中若发现使用常量则直接以符号表中的值替换(符号表不会分配内存) 。
  3. C++中定义声明的全局常量内部链接的,只能作用于当前的整个文件中,如果想让其它源文件对该常量进行访问的,必须加extern关键字将该常量转换成外部链接。
  4. 编译过程中若发现对 const 使用了 extern(外部变量)或者 &取地址操作符,则给对应的常量分配存储空间(兼容 C),这块空间用常量的值进行初始化。例如有常量a,当对a变量取地址的时候,C++编译器会为a单独的开辟一块内存空间,int* p = (int *)&a;然后这个内存空间赋给指针p,就是p指向这个内存空间。后续修改p指向的空间的值的时候,和常量a没有任何关系。

C 语言中 const 变量是只读变量,有自己的存储空间;

C++中的const常量,可能分配存储空间,也可能不分配存储空间;

  • const常量取地址时会分配空间
  • 使用变量的形式初始化const修饰的变量时会分配空间
  • const修饰自定义数据类型(结构体、对象)时会分配空间
int a = 200;
const int b = a;  // 系统直接为b开辟空间,不会把b放入到符号表中
int* p = (int*)&b;
*p = 3000;
cout << "*p = " << *p << endl;  // 3000
cout << "b = " << b << endl;  // 3000

// 4. const修饰自定义类型的变量,系统会分配空间
const Person per = { 100, "viktor" };

const除了修饰常量外,还可以有 变量只读 的意思。例如:

void func(const int num)
{
    const int count = 24;
    int array[num];            // error,num是一个只读变量,不是常量
    int array1[count];         // ok,count是一个常量

    int a1 = 520;
    int a2 = 250;
    const int& b = a1;    //b 是一个常量的引用,所以 b 引用的变量是不能被修改的
    b = a2;                         // error
    a1 = 1314; //但 const 对于变量 a1 是没有任何约束的,a1 的值变了 b 的值也就变了
    cout << "b: " << b << endl;     // 输出结果为1314
}
  1. 参数 num 表示这个变量是只读的,但不是常量
  2. count 是一个常量,因此可以使用这个常量来定义一个静态数组
  3. 变量只读并不等价于常量
  4. 引用 b 是只读的,但是并不能保证它的值是不可改变的,也就是说它不是常量

const与#define:

  1. const是在编译期间确定好的;#define是在预处理阶段处理好的。
  2. const 常量是由 编译器处理的,提供类型检查和 作用域检查,const有类型,可进行编译器类型安全检查;#define无类型,不能进行类型检查,宏定义由预处理器处理,单纯的文本替换。
  3. 宏的作用域是当前的整个文件,const的作用域以定义的情况决定
void fun1()
{
    #define a 10
    const int b = 20;
    // #undef a 。卸载宏 a
    // # undef .卸载所有宏,将宏定义作用域限制在 fun1 里(卸载宏)
}
void fun2()
{
    printf("a = %d\n", a);    //可以使用,a只是单纯的饿文本替换而已
    //printf("b = %d\n", b);  //不能使用,超出了b的作用域
}

 constexpr

  1. C++11 中添加了一个新的关键字 constexpr,这个关键字是用来修饰常量表达式的。
  2. 所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。 
  3. 常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率;因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
  4. C++11 中添加了 constexpr 关键字之后就可以在程序中使用它来修饰常量表达式,用来提高程序的执行效率。在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr
  5. 定义常量时const 和 constexpr 是等价的,都可以在程序的编译阶段计算出结果。

对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。

例如:

// 此处的constexpr修饰是无效的
constexpr struct Test
{
    int id;
    int num;
};


//要定义一个类常量对象,可以这样写
struct Test
{
    int id;
    int num;
};

int main()
{
    constexpr Test t{ 1, 2 };
    constexpr int id = t.id;
    constexpr int num = t.num;
   
    // t 是一个常量,因此它的成员也是常量
    t.num += 100;  // error,不能修改常量
    cout << "id: " << id << ", num: " << num << endl;

    return 0;
}

为了提高 C++ 程序的执行效率,我们可以将程序中值不需要发生变化的变量定义为常量。

  • 1、使用 constexpr 修饰函数的返回值,这种函数被称作常量表达式函数。constexpr 修饰的函数的返回值,必须要满足以下几个条件:
    • 函数必须要有返回值,并且 return 返回的表达式必须是常量表达式。
      constexpr int func1()
      {
          constexpr int a = 100;
          return a;
      }
    • 整个函数的函数体中,不能出现非常量表达式之外的语句(using 指令、typedef 语句以及 static_assert 断言、return 语句除外)。

      // error
      constexpr int func1()
      {
          constexpr int a = 100;
          constexpr int b = 10;
          for (int i = 0; i < b; ++i)
          {
              cout << "i: " << i << endl;
          }
          return a + b;
      }
      
      // ok
      constexpr int func2()
      {
          using mytype = int;
          constexpr mytype a = 100;
          constexpr mytype b = 10;
          constexpr mytype c = a * b;
          return c - (a + b);
      }

 2、constexpr 修饰模板函数

如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。

#include <iostream>
using namespace std;

struct Person 
{
    const char* name;
    int age;
};

// 定义函数模板
template<typename T>
constexpr T dispaly(T t) 
{
    return t;
}

int main()
{
    struct Person p { "luffy", 19 };
    //普通函数
    struct Person ret = dispaly(p); //p是变量,实例化后的函数不是常量表达式函数,此时 constexpr 是无效的
    cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;

    //常量表达式函数
    constexpr int ret1 = dispaly(250); //250是常量,符合常量表达式函数的要求,此时 constexpr 是有效的
    cout << ret1 << endl;

    constexpr struct Person p1 { "luffy", 19 }; //p1是常量
    constexpr struct Person p2 = dispaly(p1);//参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的
    cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl;
    return 0;
}

四、范围for,new内存动态分配,nullptr

(1)范围for

用于遍历一个序列:

int v[]{ 1,2,3,4,5 };
for (auto x : v); //数组v中每个元素依次放入x中(会多一个拷贝动作)
    //改造:
    for(auto &x:v); //没有了额外的拷贝动作,提高了效率

(2)new内存动态分配

栈区:局部变量,编译器自动分配和释放,空间有限

 堆区:new/malloc分配,由delete/free释放。忘记释放后,系统会回收

全局/静态存储区:放全局变量和静态变量static,程序结束时系统释放

常量存储区:常量字符串

程序代码区:放程序代码段

指针=new 类型标识符(初始值) //分配内存的同时赋初值  释放:delete 指针

指针=new 类型标识符[num]  //分配一个数组  释放:delete 指针[]

int* p = new int(18);
cout << *p << endl;
delete p;

p=new int[10];
delete p[]

 (3)nullptr(真正的指针类型)

nullptr是c++11中引入的新关键字,代表的也是空指针;使用nullptr能避免在整数和指针之间发生混淆NULL和nullptr实际上是不同的类型; 尽量用nullptr取代NULL。

cout <<"typeid(NULL).name():"<< typeid(NULL).name() << endl;//打印类型的名字
cout << "typeid(nullptr).name():" << typeid(nullptr).name() << endl;

 运行结果:


nullptr 详细探索:

在C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

char *ptr = 0;
char *ptr = NULL;

在底层源码中 NULL 这个宏是这样定义的:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

也就是说如果 源码是 C++ 程序 NULL 就是 0,如果是 C 程序 NULL 表示 (void*)0

 这是因为 C++ 中,void * 类型无法隐式转换为其他类型的指针,此时使用 0 代替 ((void *)0),用于解决空指针的问题。这个 0(0x0000 0000)表示的就是虚拟地址空间中的 0 地址,这块地址是只读的

C++中,NULL带来的问题:

  • C++ 中将 NULL 定义为字面常量 0,并不能保证在所有场景下都能很好的工作,比如,函数重载时,NULL 和 0 无法区分
    #include <iostream>
    using namespace std;
    
    void func(char *p)
    {
        cout << "void func(char *p)" << endl;
    }
    
    void func(int p)
    {
        cout << "void func(int p)" << endl;
    }
    
    int main()
    {
        func(NULL);   // 想要调用重载函数 void func(char *p)
        func(250);    // 想要调用重载函数 void func(int p)
    
        return 0;
    }
    

    运行结果:

     通过运行的结果可以看到,虽然调用 func(NULL); 但最终链接到的还是 void func(int p) ,和预期是不一样的,其实这个原因前边已经说的很明白了,在 C++ 中 NULL 和 0 是等价的


出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改,而是另其炉灶,引入了一个新的关键字 nullptr。nullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:

int*    ptr1 = nullptr;
char*   ptr2 = nullptr;
double* ptr3 = nullptr;

对应上面的代码编译器会分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型

 使用 nullptr 可以很完美的解决上边提到的函数重载问题:

#include <iostream>
using namespace std;

void func(char *p)
{
    cout << "void func(char *p)" << endl;
}

void func(int p)
{
    cout << "void func(int p)" << endl;
}

int main()
{
    func(nullptr);
    func(250);
    return 0;
}

 通过输出的结果可以看出,nullptr 无法隐式转换为整形,但是可以隐式匹配指针类型。在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮

五、结构,权限修饰符,类简介

(1)结构

c++中的结构体可以定义函数; 缺省都是public修饰的。

(2)权限修饰符

public :public修饰的变量和成员能够被外界访问,相当于类的外部接口
private :只能在内部定义的成员函数访问
protected :只能在类内部定义的成员函数访问继承的子类内部访问

(3)类简介

c++类内部默认访问级别都是private

类的定义代码一般放在.h头文件中头文件名可以和类名重名(student.h)

类的具体实现代码,放在.cpp文件中(student.cpp) 

六、函数新特性,内联函数,const详解

(1)函数回顾与后置返回类型、auto与decltype

返回值类型后置初见

void fun(int a, int)
{
            return;
}

 函数定义中,形参如果在函数体内用不到的话,则可以不给形参变量名字,只给其类型
调用时:必须fun(1, 2)
 c++11中提供了函数后置返回类型:函数声明和定义中,把返回类型放在参数列表之后 

auto fun(int a, int b)->void; //函数声明
auto fun(int a, int b)->void    //函数定义
{
  // auto 就作为一个占位符的意思,没其他意义
  //前面放auto,表示函数返回类型放在参数列表之后,而放在参数列表之后的返回类型是通过 -> 开始的。
  
}

auto与decltype详细解析

decltype关键字:

decltype被称作类型说明符,它的作用是获取表达式的类型。在编译时推导出一个表达式的类型,并且不会计算表达式的值

int x = 0;
decltype(x) y = 1; 			// y ->  int
decltype(x+y) z = x + y; 		// z - >  int	
 
const int& i = x;
decltype(i) j = y;			// j -> const int&
 
const decltype(z) *p = &z;	// p-> const int *
decltype(z) *pi = &z; 		// pi -> int*
decltype(pi) *pp = π		//pp -> int**

auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

auto varname = value;
decltype(exp) varname = value;

auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。另外,auto 要求变量必须初始化,而 decltype 不要求 。

原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void。

当使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:

  • 1、如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  • 2、如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 3、如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
using namespace std;

class Base
{
public:
    int x;
};
int& func_int_r(int, char);  //返回值为 int&

int main()
{
    const Base obj;
    int n=10;
    
    decltype(func_int_r(100, 'A')) a = n;  //a 的类型为 int&
    decltype(n) a = n;  //n 为 int 类型,a 被推导为 int 类型

    //带有括号的表达式
    decltype(obj.x) a = 0;  //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
    decltype((obj.x)) b = a;  //obj.x 带有括号,符合推导规则三,b 的类型为 int&。

    //加法表达式
    int n = 0, m = 0;
    decltype(n + m) c = 0;  //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
    decltype(n = n + m) d = c;  //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&

    return 0;
}

左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;

右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。

有一种很简单的方法来区分左值和右值,对表达式取地址,如果编译器不报错就为左值,否则为右值。


auto和decltype对 cv 限定符的处理

「cv 限定符」是 const 和 volatile 关键字的统称:

  • const 关键字用来表示数据是只读的,也就是不能被修改;
  • volatile 和 const 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取

 在推导变量类型时,auto 和 decltype 对 cv 限制符的处理是不一样的。decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符

以下是 auto 关键字对 cv 限定符的推导规则

  • 如果表达式的类型 不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。
  • 如果表达式的类型 是指针或者引用,auto 将保留 cv 限定符
//非指针非引用类型
const int n1 = 0;

auto n2 = n1; //auto将n2推断为int类型,因为n1不是指针或者引用类型
n2 = 99;  //赋值不报错

decltype(n1) n3 = 20;    //decltype把n3推断为const int 类型,保留CV限定符
n3 = 5;  //赋值报错

//指针类型
const int *p1 = &n1;

auto p2 = p1;    //p1是指针类型,auto将保留CV限定符
*p2 = 66;  //赋值报错

decltype(p1) p3 = p1;
*p3 = 19;  //赋值报错

auto和decltype对 引用的处理

表达式的类型为引用时,auto 和 decltype 的推导规则也不一样;

decltype 会保留引用类型,而 auto 会抛弃引用类型,直接推导出它的原始类型

#include <iostream>
using namespace std;

int main() 
{
    int n = 10;
    int &r1 = n;

    //auto推导
    auto r2 = r1;    //表达式类型为引用时,auto会抛弃引用类型,推导出原始类型。r2是int类型
    r2 = 20;    //r2会重新开辟一段空间,修改r2和n、r1没有任何关系
    cout << n << ", " << r1 << ", " << r2 << endl;

    //decltype推导
    decltype(r1) r3 = n; //decltype推导会保留引用类型
    r3 = 99; //修改r3会同时改变n和r1
    cout << n << ", " << r1 << ", " << r3 << endl;

    return 0;
}

 返回值类型后置解决的问题

在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:

template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t+u;
}
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);

我们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b) 直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。

那么,在 add 函数的定义上能不能直接通过 decltype 拿到返回值呢?

// 样写是编译不过的。
//因为 t、u 在参数列表中,而 C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在。
template <typename T, typename U>
decltype(t + u) add(T t, U u)  // error: t、u尚未定义
{
    return t + u;
}

//这样写就ok
//但是虽然成功地使用 decltype 完成了返回值的推导,但写法过于晦涩,
// 会大大增加 decltype 在返回值类型推导上的使用难度并降低代码的可读性。
template <typename T, typename U>
decltype((*(T*)0) + (*(U*)0)) add(T t, U u)
{
    return t + u;
}

 因此,在 C++11 中增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}

再看一个例子:

int& foo(int& i);
float foo(float& f);

template <typename T>
auto func(T& val) -> decltype(foo(val))
{
    return foo(val);
}

如果说前一个例子中的 add 使用 C++98/03 的返回值写法还勉强可以完成,那么这个例子对于 C++ 而言就是不可能完成的任务了。
在这个例子中,使用 decltype 结合返回值后置语法很容易推导出了 foo(val) 可能出现的返回值类型,并将其用到了 func 上。

总结 

返回值类型后置语法,是为了解决函数返回值类型依赖于参数导致难以确定返回值类型的问题。有了这种语法以后,对返回值类型的推导就可以用清晰的方式(直接通过参数做运算)描述出来,而不需要像 C++98/03 那样使用晦涩难懂的写法。

(2)内联函数

C++在函数定义前,新增了inline关键字,这个普通函数就变成了内联函数

  • 优点:减少了普通函数的压栈、出栈解决了函数体很少,调用频繁的函数压栈开销问题
  • inline影响编译器,在编译阶段对inline函数进行处理,系统尝试将调用该函数的动作替换为函数本体。通过这种方式,来提升性能

inline只是开发者对编译器的一个建议,编译器可以去做,也可以不去做,这取决于编译器的诊断功能;内联函数的定义放在头文件中。这样需要用到这个内联函数的.cpp文件都能够通过#include的方式把这个内联函数的源代码包含进来。以便找到这个函数的本体源代码并尝试将该函数的调用替换为函数体内的语句

  • 缺点:代码膨胀的问题,所以内联函数的函数体尽量要小,这样内联函数才有意义
  • 各种编译器对inline的处理各不相同,inline函数要尽量简单 

 (3)函数杂合用法总结

  • 函数返回类型是void,表示函数不返回任何类型。但是我们可以调用一个返回类型是void的函数,让它作为另一个返回类型是void的函数的返回值
  •  如果一个函数不调用的话,可以只有声明,没有定义
  • 普通函数,定义只能定义一次(定义放在.cpp文件),声明可以声明多次
void fun3_1()
{

}
void fun3_2()
{
	return fun3_1();//这可以
	return;//这也可以
}

 (4)const char*、char const*、char* const 三者区别

  1. const int a == int const a;
  2. const char* p --> p指向的内存空间的内容不可修改(不能通过p修改指向的内存空间)
  3. char const* p == const char* p
  4.  char* const p --> 定义时,p必须给初值。p以后的指向就不能变了
  5. const char* const p --> p指向的内容不可通过p修改 和 p的指向也不能变

总结:const是修饰离它最近的右边的值。第二条,离const最近的是char*,所以修饰的是char*内存空间;第四条,p离const最近,所以const修饰的是指针变量p,表示p不可改变(即不可指向其他空间了)。

七、string类型介绍

(1)简介

STL标准库中的类 类型,用于可变长字符串的处理。

(2)定义和初始化string对象

string s1;	//默认初始化,s1="",空串
string s2 = "i love china";	//拷贝时不包括末尾的 \0
string s3("i love china");
string s4 = s2;	//定义的时候初始化
string s5(5, 'a'); // s5 == 5个a =="aaaaa" 。这种方式不太推荐,因为会在系统内部创建临时对象

char str[] = "i love china";
string s6(str);	//用c语言的字符数组初始化string类型

(3)string对象上的操作

判断是否为空		  :bool s1.empty()
返回字符 / 字节数量	  :UINT s1.size()、UINT s1.length()
返回s1中第n个字符	  :s1[n](下标从0开始)
字符串的连接		  :string s6 = s1 + s2; //连接得到新的string对象
字符串对象赋值		  :s1 = s2		//用s2里边的内容取代原来s1里边的内容
判断两个字符串是否相等:s1 == s2	//大小写敏感、长度相同、字符相同
判断不相等			  :s1 != s2
返回string对象中的内容指针:s1.c_str()  //返回一个指向正规的C字符串的指针常量,也就是以\0结尾的
	const char* p = s1.c_str();
string + 字面量(s1 + " abc " + s2 + 'e'),隐式类型转换:"abc"会转换成string类型

但是:string str = "abc" + "def"; 是错误的,因为def不知道该转成什么类型
string str = "abc" + s1 + "defd"; //OK,abc先和s1加,组成临时的string对象,defd再和string对象相加,defd转换成string对象

(4)读写string对象

cin >> s1; //输入:  abc  l  ,会把abc前后的空格干掉

(5)范围for针对string的使用

string可以看成是一个字符序列

string s = "i love china";
for (auto i : s)
{
	cout << i << endl;
}

八、vector类型介绍

(1)简介

集合或动态数组,Vector里面不能装引用。

(2)定义和初始化vector对象

// 1.空vector:
    vector<string> myStr1; //string类型的空容器
	myStr1.push_back("abcd");
	
// 2.元素拷贝的初始化方式
	vector<string> myStr2(myStr1);
	vector<string> myStr2=myStr1;
	vector<string> myStr2={"aaa","bbb","ccc"}; //(c++11中的列表初始化方式)
	
// 3.创建指定数量的元素,有元素数量概率的东西,一般会用圆括号()
	vector<int> v1(15,10);	//15个int元素,每个元素的值是10
	vector<int> v{10,1};    //2个元素,第一个元素是10,第二个元素是1,等同于初始化列表
	vector<int> v2(15);	    //15个int元素,没有给初值,默认为0
	vector<string> v3(15);	//15个string元素,没有给初值,默认为""
	
// 4. ()一般表示对象中元素数量这么个概率,{}一般表示元素内容概率,但不绝对
	vector<int> v1(10)	;//10个int元素
	vector<int> v2{10}	;//1个元素,元素值是10
	vector<string> v3{"hello"}; //1个元素,内容是hello
	vector<string> v4{10}; //10个元素,每个元素 ""。因为10不能隐式转换为string,所以就把它当作10个string元素
	vector<string> v5{10,"hello"};//10个元素,每个元素 "hello"

	

总结:要想正常的通过{}进行初始化,那么{}里边的的值的类型,必须和vector实例化的类型相同

(3)vector对象上的操作

  • 1.最常用的是不知道vector对象里有多少个元素。需要动态增加/减少。所以一般来讲,先创建空vector
  • 2.是否为空:v.empty();    ( vector<int> v )
  • 3.向vector对象末尾增加一个元素:v.push_back(1);
  • 4.返回元素个数:v.size();
  • 5.移除所有元素,将容器清空:v.clear();
  • 6.返回第n个元素:int a=v[n]; // n>=0&&n<v.size()
  •  7.=赋值:vector<int> vv; vv=v;    //vv得到了v中所有元素,vv原来的元素就消失了
  • 9. ==和!= :两个vector相等必须元素数量、对应位置的元素值一样才相等。否则就不相等。vv==v  ,vv!=v 

(4)范围for的应用

// 1.
vector<int> v{1,2,3,4,5};

for(auto& i:v)
{
    i*=2;
}

// 2.
vector<int> v{1,2,3,4,5};
for(auto& i:v)		//每次循环都会定义i
{
    v.push_back(123);	//导致输出乱套
	i*=2;
}
// 范围for:遍历vector容器时,不要添加或删除元素

 九、迭代器精彩演绎,失效分析及弥补,实战

(1)迭代器简介

迭代器是一种遍历容器内元素的数据类型,这种数据类型有点像指针,我们就理解为迭代器用来指向容器中的某个元素。
string、vector,[]很少用,更常用的访问方式就是用迭代器。

(2)容器的迭代器类型

vector<int> v{ 1,2,3,4,5 };
vector<int>::iterator iter;//定义迭代器
vector<int>::const_iterator iter;//定义只读迭代器

(3)迭代器begin()/end()操作,反向迭代器rbegin()/rend()操作

正向迭代器:iterator
        begin() / end()返回迭代类型
        begin()返回的迭代器,指向的是容器中的第一个元素
        end()返回的是最后一个元素之后的元素(不存在的元素),只是一个标志
        如果容器为空,begin和end返回的迭代器就相同
反向迭代器:reverse_iterator
      rbegin()返回指向最后一个元素的迭代器,rend()返回指向第一个元素之前的那个元素的迭代器。

 (4)迭代器运算符

*(iter):返回迭代器所指元素的引用
 iter++和++iter:指向容器中下一个元素(iter已经指向end()了,就不能再++)
iter1==iter2和iter1!=iter2:如果两个迭代器指向的是同一个元素就相等

(5)const_iterator常量迭代器

表示迭代器指向的元素的值不能改变 

(6)cbegin()和cend()操作

c++11引入的两个新函数,和begin\end类似,返回的都是常量迭代器 

(7)迭代器失效

使用了迭代器的循环体中,千万不要改变容器的容量。否则可能会使指向容器元素的指针、引用、迭代器失效。失效就表示不能再代表任何容器中的元素,一旦使用了失效的东西,就等于严重的程序错误,很多情况下,程序直接崩溃。

vector<int> v{ 1,2,3,4,5 };
for (vector<int>::iterator iter = v.begin(); iter != v.end(); iter++) //普通迭代器
{
    cout << *iter << " ";
}
cout << endl;

//反向迭代器:reverse_iterator
for (vector<int>::reverse_iterator iter = v.rbegin(); iter != v.rend(); iter++)
{
	cout << *iter << " ";
}
cout << endl;

//常量迭代器
// vv是个const对象,所以不能用普通迭代器指向容器的元素,因为普通迭代器可以修改元素的值
// vector<int>::iterator iter = vv.begin(); 这句是错误的
const vector<int> vv{ 1,2,3,4,5 };
for (vector<int>::const_iterator iter = vv.begin(); iter != vv.end(); iter++)
{
	cout << *iter << " ";
}
cout << endl;

//cbegin()和cend()操作
for (vector<int>::const_iterator iter = vv.cbegin(); iter != vv.cend(); iter++)
{
	cout << *iter << " ";
}
cout << endl;

//迭代器失效
cout << "迭代器失效" << endl;
for (auto iter = v.begin(); iter != v.end(); iter++)
{
	v.push_back(1);	//一旦改变容器的元素个数,立马break出去。这样程序就不会崩溃
	break;
	cout << *iter << " ";
}
cout << endl;

(8)灾难程序演示

遍历容器时,插入元素:

cout << "灾难程序演示1" << endl;
vector<int> v1{ 6,7,8,9,10 };
auto begin = v1.begin(), end = v1.end();
while (begin != end)
{
	cout << *begin << " ";
	v1.insert(begin, 5); //插入新值。参数1是插入位置,参数2是插入的数据。
	break;
		
	//会导致迭代器失效,具体是哪个迭代器失效(begin or end),取决于vector容器的实现原理,具体再查资料详细研究
	//不太明确哪个迭代器失效,最明确的做法就是一旦插入数据,插入完毕立马break出循环体
	//然后重新开一个循环体,遍历vector容器
	++begin;
}


//改善
cout << "//改善" << endl;
auto beg = v1.begin();
int icount = 0;
while (beg != v1.end())//每次更新end防止end失效
{
	beg = v1.insert(beg, icount + 80);
	icount++;
	if (icount > 10)
		break;
	++beg;
}
beg = v1.begin();
while (beg != v1.end())
{
	cout << *beg << " ";
	beg++;
}
cout << endl;

 遍历容器时,删除元素:

//灾难程序演示2
vector<int> iv{ 6,7,8,9,10 };
for (auto iter = iv.begin(); iter != iv.end(); ++iter)
{
	//	iv.erase(iter);	//erase函数,移除iter位置上的元素,返回下一个元素位置
	// 会引起崩溃,迭代器失效
}

//改善1
auto iter_beg = iv.begin();
auto iter_end = iv.end();
while (iter_beg != iv.end())//每次删除操作都会更新end,使得iv.end()才是vector容器真正的最后一个元素位置
{
	// 不能写成这样:iter_beg != iter_end,因为iter_end不会随时更新
	iter_beg = iv.erase(iter_beg);//返回iter_beg下一个元素的迭代器
}

//改善2
while (!iv.empty())
{
	auto iter = iv.begin();
	iv.erase(iter);
}

十、类型转换:static_cast,reinterpret_cast等;静态断言

类型转换

C语言中的类型转换:

//(1)隐式类型转换:系统自动进行,不需要程序开发人员介入。
int m = 3 + 45.6;  // 48.6000 -> 48 。把小数部分截掉,也属于隐式类型准换的一种行为
double n = 3 + 45.6;
//(2)显示类型转换(强制类型转换)
int k = 5 % int(3.2);//C语言风格强制类型转换

 c++强制类型转换分为4种,这4种强制类型转换分别用于不同的目地,提供更丰富的含义和功能。

(1)static_cast:静态转换

就理解成“正常转换”,编译时 ,c++编译器就会做类型检查;即所谓的静态就是在编译时就能够进行检查。和C中的强制类型转换感觉差不多,代码中要保证转换的安全性和正确性,C风格的强制类型转换和隐式类型转换都可以用静态转换。因为C++编译器在编译检查时一般都能通过。

可用于:相关类型转换(比如整形和浮点型之间的转换)、子类转父类、void*转其他类型指针

不能用于:指针之间的转换(如int*转float*)。若 不同类型之间进行强制类型转换,用 reinterpret_cast<>() 进行重新解释。


(2)dynamic_cast:动态类型转换,运行时类型识别和检查

动态类型转换,安全的 基类和子类之间转换,主要用来父类和子类之间转换用的(父类指针指向子类对象,然后用dynamic_cast把父指针类型往子指针类型转)。用于子类和父类之间的多态类型转换。运行时类型检查(函数参数父类指针,可以具体分辨父类指针指向的是哪个子类,向下转型,把老子转成小子;如果转换的类型和 实参类型 不一样,就会失败,返回一个空指针)

(3)const_cast:去除 指针或引用 的常量属性(不是指针、引用无法转变)

去除指针或者引用的常量属性,去除 const 限定的目的不是为了修改它的内容,使用 const_cast 去除 const 限定,通常是为了函数能够接受这个实际参数。

(4)reinterpret_cast:重新解释类型

编译时就会进行类型转换的检查。处理无关类型的转换,两个转换类型之间没有什么关系,就等于可以乱转。强制类型转换;将数据以二进制存在形式的重新解释。reinterpret_cast<>()很难保证移植性。

 用法:强制类型转换名<type>(express)。例如: static_cast<int>(5.8f);

// 例如:取出常量区的地址
const char* string=”aaabbb”;
cout<<” 字符串地址:”<<static_cast<const void*>(string)<<endl;

静态断言

以前使用的assert 是一个运行时断言,也就是说它只有在程序运行时才能起作用 。这意味着不运行程序我们将无法得知某些条件是否是成立的。

比如:我们想知道当前是 32 位还是 64 位平台,对于这个需求我们应该是在程序运行之前就应该得到结果,如果使用断言显然是无法做到的,对于这种情况我们就需要使用 C++11 提供的静态断言了。

静态断言 static_assert

        所谓静态就是在编译时就能够进行检查的断言,使用时不需要引用头文件。静态断言的另一个好处是,可以自定义违反断言时的错误提示信息。静态断言使用起来非常简单,它接收两个参数:

  • 参数1:断言表达式,这个表达式通常需要返回一个 bool值
  • 参数2:警告信息,它通常就是一段字符串,在违反断言(表达式为false)时提示该信息
// assert.cpp
#include <iostream>                                         
using namespace std;
  
int main()
{
    
    static_assert(sizeof(long) == 4, "错误, 不是32位平台..."); //如果表达式 sizeof(long) == 4 结果为false,则出现报错信息“错误, 不是32位平台...”
    cout << "64bit VS2019 指针大小: " << sizeof(char*) << endl;
    cout << "64bit VS2019 long 大小: " << sizeof(long) <<endl;//Windows的64位,long是4byte
  
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

心之所向便是光v

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

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

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

打赏作者

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

抵扣说明:

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

余额充值