【C++篇】C++与C小知识点区别

友情链接:C/C++系列系统学习目录

知识点内容正确性以C++ Primer(中文版第五版)、C++ Primer Plus(中文版第六版)为标准,同时参考其它各类书籍、优质文章等,总结归纳出个人认为较有逻辑的整体框架,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上



🚀一、C++提供默认参数

void scoreAdd2(int score[], int n, int val=5) {
    for (int i=0; i<n; i++) {
    	score[i] += val;
    }
}
  • 默认参数, 只能出现在参数列表的最后, 即默认参数后面, 不能有普通参数

  • C 语言不支持函数的默认参数!

  • 对于带参数列表的函数,必须从右往左添加默认值

默认参数不能同时存在于函数声明和函数定义中。

🚀二、C++11的初始化

通过使用C++新增的大括号初始化器,初始化常规变量的方式与初始化类变量的方式更像。C++11使得可将大括号初始化器用于任何类型(可以使用等号,也可以不使用),这是一种通用的初始化语法

  1. 可以省略等号

  2. 大括号内不包含任何东西,这把所有的元素都初始化为0

  3. 列表初始化禁止缩窄转换:例如将浮点数转换为整型是缩窄操作,即使浮点数的小数点后面为零。或1122011超出了char变量的取值范围(这里假设char变量的长度为8位)

    C++标准模板库(STL)提供了一种数组替代品—模板类vector,而C++11新增了模板类array。这些替代品比内置复合类型数组更复杂、更灵活

int owls = 101;           //1.来自C语言
int wrens(432);           //2.可选的C++语法

//3.C++11初始化方式,叫做大括号初始化器,用于数组和结构,但在C++98中,也用于单值变量,在C++11中用的比较多,
int hamburgers = {24};  

//可以省略等号:
int hamburgers{24};   

//大括号内不包含任何东西,变量将被初始化为0
int rocs = {};

例子:

//数组初始化:
double earnings[4] {1.2e4 , 1.6e4 , 1.1e4 , 1.7e4};  //等号可选,大括号内不包含任何东西,这把所有的元素都初始化为0

//C风格字符串和string对象初始化:
char first_date[] = {"Le Chapon Dodu " } ;
char second_date[] { "The Elegant Plate" } ;
string third_date = {"The Bread Bowl " };
string fourth_date { "Hank's Fine Eats "} ;

//结构初始化:
inflatable duck = {"Daphne " , 0.12,9.98} ;  //C++方式省略了struct

//类初始化:
//创建对象jock时,第二和第三个参数将为默认值0和0.0,第三个声明与默认构造函数匹配,因此将使用该构造函数创建对象temp
Stock hot_tip = { "Derivatives Plus Plus", 100,45.0 } ;
stock jock { "sport Age Storage,Inc" } ;
stock temp {};

🚀三、C++11基于范围的for循环

C++11新增了一种循环:基于范围(range-based)的for循环。这简化了一种常见的循环任务:对数组(或容器类,如vector和array)的每个元素执行相同的操作

double prices [5] = {4.99,10.99,6.87,7.99,8.49} ;
for (double x : prices)
	cout << x << endl;

//结合初始化列表:
for (double x : {4.99,10.99,6.87,7.99,8.49})
	cout << x << endl;

x最初表示数组prices的第一个元素。显示第一个元素后,不断执行循环,而x依次表示数组的其他元素。因此,上述代码显示全部5个元素,每个元素占据一行。总之,该循环显示数组中的每个值。

  1. 要修改数组的元素,需要使用不同的循环变量语法:
//符号&表明x是一个引用变量,这种声明让接下来的代码能够修改数组的内容,而第一种语法不能。
for (double &x : prices)
	x = x*0.80 ;//20% off sale
  1. 这种循环主要用于模板类:
void traverse_for(vector<int> vec) {
	for(int i:vec) {
		cout<<i<<endl;
	}
}

void traverse_for(map<string, int> my_map) {
	for(pair<string, int> m:my_map) {
		cout<<m.first<<":"<<m.second<<endl;
	}
}

//使用迭代器遍历容器:
void traverse_iter(vector<int> vec) {
	vector<int>::iterator it = vec.begin();
	while(it!=vec.end()) {
		cout<<*it<<endl;
		it++;
    }
}

void traverse_iter(map<string, int> my_map) {
	map<string, int>::iterator it=my_map.begin();
	while(it!=my_map.end()) {
		cout<<it->first<<":"<<it->second<<endl;
		it++;
    }
}

🚀四、C与C++数据类型上的区别

数据类型分为两组:基本类型和复合类型,基本类型即整数和浮点型,在基本类型的基础上创建的复合类型,包括数组、字符串、指针和结构,char类型也属于整型,影响最深的复合类型是类

⛳(一)基本类型区别

  1. bool类型:

    • C语言中的布尔类型为_Bool,stdbool.h头文件让bool成为_Bool的别名,而且还把true和false分别定义为1和0的符号常量
    • C++语言中的布尔类型为bool,true和false定义为关键字而不是符号常量
  2. C++强制类型转换:

    (int)term;  //来自C语言
    term(int);  //C++新增
    

    C++还定义了四个强制类型转换运算符,在后面单独归纳

  3. auot声明(C++11):

    C++11新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。为此,它重新定义了auto的含义。auto是一个C语言关键字,在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:

    auto n = 100 ;    // n is int
    auto x = 1.5;     // x is double
    auto y = 1.3e12L; // y is long double
    

    用于标准模板库:

    std::vector<double> scores;
    std::vector<double>::iterator pv = scores.begin();
    
    //C++11允许重写为:
    std::vector<double> scores;
    auto pv = scores. begin();
    

    下面的示例演示了C++11版本的auto提供的方便。编译器知道arp的类型,能够正确地推断出ppb的类型:

    const antarctica_years_end * arp[3] = {&s01,&s02,&s03 } ;
    const antarctica _years_end **ppa = arp;
    auto ppb = arp; // C++11 automatic type deduction
    

    自动类型推断只能用于单值初始化,而不能用于初始化列表

⛳(二)复合类型区别

  1. C++允许在声明数组大小时使用const整数,而C却不允许。

  2. C++允许在声明结构变量时省略关键字struct:

    在C++中,结构标记的用法与基本类型名相同。这种变化强调的是,结构声明定义了一种新类型。在C++中,省略struct不会出错。

    struct inflatable goose;   //keyword struct required in C
    inflatable vincent ;      // keyword struct not required in C++
    
  3. C++结构除了成员变量之外,还可以有成员函数。但这些高级特性通常被用于类中,而不是结构中,

  4. 传统的枚举存在一些问题,其中之一是两个枚举定义中的枚举量可能发生冲突。假设有一个处理鸡蛋和T恤的项目,其中可能包含类似下面这样的代码:

    enum egg { Small, Medium,Large,Jumbo } ;
    enum t_shirt { Small,Medium,Large,xlarge } ;
    

    这将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。为避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:

    enum class egg { Small, Medium,Large,Jumbo } ;
    enum class t_shirt { Small,Medium,Large,xlarge } ;
    

    也可使用关键字struct代替class。无论使用哪种方式,都需要使用枚举名来限定枚举量:

    egg choice = egg::Large ; // the Large enumerator of the egg enum
    t_shirt Floyd = t_shirt::Large; // the Large enumerator of the t_shirt enum
    
  5. C++11空指针:

    在C++98中,字面值0有两个含义:可以表示数字值零,也可以表示空指针,这使得阅读程序的人和编译器难以区分。有些程序员使用(void *) 0来标识空指针(空指针本身的内部表示可能不是零),还有些程序员使用NULL,这是一个表示空指针的C语言宏。C++11提供了更好的解决方案:引入新关键字nullptr,用于表示空指针。您仍可像以前一样使用0——否则大量现有的代码将非法,但建议您使用nullptr

  6. C++标准提供了一种在失败时返回空指针的new,

    int * pi = new (std::nothrow) int ;
    int * pa = new (std::nowthrow} int [500];
    
  7. 数组的替代品:

    C++98新增的标准模板库(STL)提供了模板类vector,它是动态数组的替代品。C++11提供了模板类array,它是定长数组的替代品。

⛳(三)C++11新增

🎈1.C++11智能指针

为什么要使用智能指针:

#include <iostream>
#include <string>
#include <exception>

using namespace std;

void memory_leak_demo1() {
    string* str = new string("今天又敲了一天代码,太累了,回家休息了!!!");
    
    cout << *str << endl; //没有delete,造成内存泄漏
    return;
}

int memory_leak_demo2() {
    string* str = new string("这个世界到处是坑,所以异常处理要谨记在心!!!");
    
    /***********************************************
    * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
    * 的文件中读取某些数据,而文件此时不存在
    ************************************************/
    {
    	throw exception("文件不存在");  
    }
    
    //抛出异常后,以下代码不会执行,也会造成内存泄漏
    cout << *str << endl;
    delete str;
    return 0;
}

int main()
{
    memory_leak_demo1();
    try {
    	memory_leak_demo2();
	}
	catch (exception e) {
	cout<<"catch exception: "<<e.what()<<endl;
    }
    
    system("pause");
    return 0;
}

以上两种情况都会出现内存泄漏!解决方案: 把 string 定义为 auto 变量,在函数生命周期结束时释放:

void memory_leak_demo1() {
    string str("今天又敲了一天代码,太累了,回家休息了!!!"); //str对象在此函数返回后会自动调用其析构函数释放内存,string类构造函数中是动态分配内存的
    cout << str << endl;
    return;
}

int memory_leak_demo2() {
    string str("这个世界到处是坑,所以异常处理要谨记在心!!!");
    
    /***********************************************
    * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
    * 的文件中读取某些数据,而文件此时不存在
    ************************************************/
    {
    throw exception("文件不存在"); //不论在哪结束,函数返回,str局部变量销毁,都会自动调用其析构函数释放动态分配的内存
    }
    
    cout << str << endl;
    return 0;
}

思考:

如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针:

智能指针就是通过这个原理来解决指针自动释放的问题!C++98 提供了 auto_ptr 模板的解决方案,C++11 增加 unique_ptr、shared_ptr 和 weak_ptr

(1)auto_ptr

auto_ptr 是 c++ 98 定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用 delete 来释放内存!

//头文件: 
#include <memory>
//用 法: 
auto_ptr<类型> 变量名(new 类型)
    
//例 如:
auto_ptr<string> str(new string("我要成为大牛~ 变得很牛逼!"));
auto_ptr<vector<int>> av(new vector<int>(10));
#include <iostream>
#include <string>
#include <exception>
#include <memory>

using namespace std;

//auto_ptr< Test> t(new Test()); //忠告 1: 智能指针不要定义为全局变量,因为t这个对象等到程序结束后才会被析构(无意义)

class Test
{
public:
    Test() {
        cout << "Test is construct" << endl;
        debug = 1;
    }
    
    ~Test() { cout << "Test is destruct" << endl; }
    
    int getDebug() {
        return debug;
    }
private:
    int debug;
};

//用 法: auto_ptr<类型> 变量名(new 类型)

void memory_leak_demo1() {
	auto_ptr<Test> t(new Test());
	//忠告 2: 除非自己知道后果,不要把 auto_ptr 智能指针赋值给同类型的另外一个智能指针
	//auto_ptr< Test> t1;
	//t1 = t;

	//auto_ptr<Test>* tp = new auto_ptr<Test>(new Test()); //忠告 3: 不要定义指向智能指针对象的指针变量

    //在使用智能指针访问对象时,使用方式和普通指针一样,因为其内部进行了重载
    cout<< "-> debug: "<<t->getDebug()<< endl;
    cout << "* debug: " << (*t).getDebug() << endl;
    
    //该模板类提供的接口
    //Test* tmp = t.get();  
    //cout << "get debug: " << tmp->getDebug() << endl;
    
    //release 取消指针对动态内存的托管,之前分配的内存必须手动释放
    //Test* tmp = t.release();
    //delete tmp;
    
    //reset 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
    //t.reset();  什么都不传就是空指针
    t.reset(new Test());   //重置内存地址,原来的会被析构掉,所以函数返回后总共会调用两次析构函数
    
    if(0){
        Test* t1 = new Test();
        t1->getDebug();
    } 
    
    return;
    
}
    
int memory_leak_demo2() {
    //Test* t = new Test();
    auto_ptr< Test> t(new Test());
    
    /***********************************************
    * 程序执行一段复杂的逻辑,假设尝试从一个必须存在
    * 的文件中读取某些数据,而文件此时不存在
    ************************************************/
    {
    throw exception("文件不存在");
    }
    
    //delete t;
    return 0;
}
  • 尽可能不要将 auto_ptr 变量定义为全局变量或指针
  • 除非自己知道后果,不要把 auto_ptr 智能指针赋值给同类型的另外一个智能指针
  • C++11 后 auto_ptr 已经被“抛弃”,已使用 unique_ptr 替代!
(2)unique_ptr

auto_ptr 是用于 C++11 之前的智能指针。由于 auto_ptr 基于排他所有权模式,即:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有两大问题:

  • 复制和赋值会改变资源的所有权,不符合人的直觉。
  • 在 STL 容器中使用 auto_ptr 存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
  • 不支持对象数组的操作
#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>

using namespace std;

int main() {

    //弊端 1. auto_ptr 被 C++11 抛弃的主要理由 p1= p2 ,复制或赋值都会改变资源的所有权
    auto_ptr<string> p1(new string("I 'm martin."));
    auto_ptr<string> p2(new string("I 'm rock."));
    printf("p1: %p\n", p1.get());
    printf("p2: %p\n", p2.get());
    
    p1 = p2;
    printf("after p1 = p2\n");
    printf("p1: %p\n", p1.get());
    printf("p2: %p\n", p2.get());
    
    //弊端 2. 在 STL 容器中使用 auto_ptr 存在重大风险,因为容器内的元素必需支持可复制(copyconstructable)和可赋值(assignable)。
    vector<auto_ptr<string>> va;
    auto_ptr<string> p3(new string("I 'm p3."));
    auto_ptr<string> p4(new string("I 'm p4."));
    
    va.push_back(std::move(p3));
    va.push_back(std::move(p4));
    
    cout <<"va[0]: "<< *va[0] << endl;
    cout <<"va[1]: "<< *va[1] << endl;
    
    //风险来啦
    va[0] = va[1];
    cout << "va[0]: " << *va[0] << endl;
    cout << "va[1]: " << *va[1] << endl;
    
    //弊端 3. 不支持对象数组的内存管理
    //auto_ptr<int[]> ai(new int[5]); //不能这样定义
    
    //auto_ptr 陷阱,不能把同一段内存交给多个 auto_ptr 变量去管理
    /*{
        auto_ptr<string> p2;
        
        string* str = new string("智能指针的内存管理陷阱");
        p2.reset(str);
        {
            auto_ptr<string> p1;
            p1.reset(str);
        }
        cout <<"str: " << *p2 << endl;
    }*/
    
    system("pause");
    return 0;
}

所以,C++11 用更严谨的 unique_ptr 取代了 auto_ptr!

unique_ptr 特性:

  • 基于排他所有权模式:两个指针不能指向同一个资源
  • 无法进行左值 unique_ptr 复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  • 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  • 在容器中保存指针是安全的
#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>

using namespace std;

int main() {
    //弊端 1. auto_ptr 被 C++11 抛弃的主要理由 p1= p2 ,复制或赋值都会改变资源的所有权
    //unique_ptr 如何解决这个问题? 不允许显示的右值赋值和构造
    unique_ptr<string> p1(new string("I 'm martin."));
    unique_ptr<string> p2(new string("I 'm rock."));
    printf("p1: %p\n", p1.get());
    printf("p2: %p\n", p2.get());
    
    //如果一定要转移,使用 move 把左值转成右值
    p1 = std::move(p2);
    printf("p1: %p\n", p1.get());
    printf("p2: %p\n", p2.get());
    
    //p1 = p2; //左值赋值禁止
    unique_ptr<string> p3(new string("I 'm p3."));
    unique_ptr<string> p4(std::move(p3)); //左值拷贝构造也不行,必须转成右值

    //弊端 2. 在 STL 容器中使用 auto_ptr 存在重大风险,因为容器内的元素必需支持可复制(copy
    constructable)和可赋值(assignable)。
    vector<unique_ptr<string>> vu;
    unique_ptr<string> p5(new string("I 'm p5."));
    unique_ptr<string> p6(new string("I 'm p6."));
    
    vu.push_back(std::move(p3));
    vu.push_back(std::move(p4));
    
    cout << "va[0]: " << *vu[0] << endl;
    cout << "va[1]: " << *vu[1] << endl;
    
    //vu[0] = vu[1]; //unique_ptr 不支持直接赋值,没有风险
    
    //弊端 3. auto_ptr 不支持对象数组的内存管理,unique_ptr 支持
    //但是 unique_ptr 支持对象数组的管理
    //auto_ptr<int[]> ai(new int[5]); //不能这样定义
    unique_ptr<int[]> ui(new int[5]); //自动会调用 delete []函数去释放
    
    system("pause");
    return 0;
}

构造函数

unique_ptr<T> up ; //空的 unique_ptr,可以指向类型为 T 的对象
unique_ptr<T> up1(new T()) ;//定义 unique_ptr,同时指向类型为 T 的对象
unique_ptr<T[]> up ; //空的 unique_ptr,可以指向类型为 T[的数组对象
unique_ptr<T[]> up1(new T[]) ;//定义 unique_ptr,同时指向类型为 T 的数组对象
unique_ptr<T,D> up(); //空的 unique_ptr,接受一个 D 类型的删除器 d,使用 d 释放内存
unique_ptr<T,D> up(new T()); //定义 unique_ptr,同时指向类型为 T 的对象,接受一个 D类型的删除器 d,使用删除器 d 来释放内存

赋值

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(new int(11));
up1 = std::move(up2);//必须使用移动语义,结果,up1 内存释放, up2 交由 up1 管理

主动释放对象

up = nullptr ;//释放 up 指向的对象,将 up 置为空
up = NULL; //作用相同

放弃对象控制权

up.release(); //放弃对象的控制权,返回指针,将 up 置为空,不会释放内存

重置

up.reset()//参数可以为 空、内置指针,先将 up 所指对象释放,然后重置 up 的值

交换

up.swap(up1); //将智能指针 up 和 up1 管控的对象进行交换
(3)shared_ptr

熟悉了 unique_ptr 后,其实我们发现 unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?

如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加 1,当智能指针析构时,引用计数减 1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!

在这里插入图片描述

构造函数

shared_ptr<T> sp ; //空的 shared_ptr,可以指向类型为 T 的对象
shared_ptr<T> sp1(new T()) ;//定义 shared_ptr,同时指向类型为 T 的对象
shared_ptr<T[]> sp2 ; //空的 shared_ptr,可以指向类型为 T[的数组对象 C++17 后支持
shared_ptr<T[]> sp3(new T[]{...}) ;//指向类型为 T 的数组对象 C++17 后支持
shared_ptr<T> sp4(NULL, D()); //空的 shared_ptr,接受一个 D 类型的删除器,使用 D释放内存
shared_ptr<T> sp5(new T(), D()); //定义 shared_ptr,指向类型为 T 的对象,接受一个 D类型的删除器,使用 D 删除器来释放内存

初始化

方式一 构造函数

shared_ptrr<int> up1(new int(10)); //int(10) 的引用计数为 1
shared_ptrr<int> up2(up1); //使用智能指针 up1 构造 up2, 此时 int(10) 引用计数为 2

方式二 使用 make_shared 初始化对象,分配内存效率更高

make_shared 函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr; 用法:

make_shared<类型>(构造类型对象需要的参数列表);

shared_ptr<int> p4 = make_shared<int>(2); //多个参数以逗号','隔开,最多接受十个
shared_ptr<string> p4 = make_shared<string>("字符串");

赋值

shared_ptrr<int> up1(new int(10)); //int(10) 的引用计数为 1
shared_ptr<int> up2(new int(11)); //int(11) 的引用计数为 1
up1 = up2;//int(10) 的引用计数减 1,计数归零内存释放,up2 共享 int(11)给 up1, int(11)的引用计数为 2

主动释放对象

shared_ptrr<int> up1(new int(10));
up1 = nullptr ;//int(10) 的引用计数减 1,计数归零内存释放
up1 = NULL; //作用同上

重置

up.reset() ; //将 p 重置为空指针,所管理对象引用计数 减 1
up.reset(p1); //将 p 重置为 p1(的值),p 管控的对象计数减 1,p 接管对 p1 指针的管控
up.reset(p1,d); //将 p 重置为 p(的值),p 管控的对象计数减 1 并使用 d 作为删除器

交换

std::swap(p1,p2); //交换 p1 和 p2 管理的对象,原对象的引用计数不变
p1.swap(p2); //同上

使用陷阱

shared_ptr 作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>

using namespace std;

class girl;

class boy {
public:
    boy() {
    	cout << "boy construct!" << endl;
    }
    
    ~boy() {
    	cout << "boy destruct!" << endl;
    }
    
    void set_girl_friend(shared_ptr<girl> &g) {
    	girl_friend = g;
    }
private:
	shared_ptr<girl> girl_friend;
};

class girl {
public:
    girl() {
    	cout << "girl construct!" << endl;
    }
    
    ~girl() {
    	cout << "girl destruct!" << endl;
	}
	
    void set_boy_friend(shared_ptr<boy> &b) {
    boy_friend = b;
    }
private:
    shared_ptr<boy> boy_friend;
};
    
void use_trap() {
    shared_ptr<girl> sp_girl(new girl());//白娘子
    shared_ptr<boy> sp_boy(new boy()); //许仙
    
    sp_girl->set_boy_friend(sp_boy);
    sp_boy->set_girl_friend(sp_girl);
}

int main() {
    use_trap();
    system("pause");
    return 0;
}

解释:sp_girl管理girl对象,girl_friend接受引用也管理girl对象,use_trap函数结束后,其中两个共享指针被析构

(4)weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少. 同时 weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

在这里插入图片描述

🎈2.C++11类型转换

(旧式转型)C风格的强制类型转换:

TYPE b = (TYPE) a
//例如:
int i = 48;
char c = (char) i;

(新式转型) C++风格的类型转换提供了 4 种类型转换操作符来应对不同场合的应用。

//格式:
TYPE b = 类型操作符<TYPE>(a)

类型操作符:static_cast | reinterpreter_cast | dynamic_cast | const_cast
(1)static_cast

静态类型转换(斯文的劝导,温柔的转换)。如 int 转换成 char

主要用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。上行指针或引用(派生类到基类)转换安全,下行不安全
  • 用于基本数据类型之间的转换,如把 int 转换成 char,把 int 转换成enum。这种转换的安全性也要开发人员来保证。
  • 把空指针转换成目标类型的空指针。
  • 把任何类型的表达式转换成 void 类型。
#include <iostream>

using namespace std;
//抽象类
class Animal {
public:
	virtual void cry() = 0;
};

class Cat : public Animal
{
public:
	void cry()
    {	
    	cout << "喵喵瞄" << endl;
    }
};

class Dog : public Animal
{
public:
    void cry()
    {
    	cout << "汪汪汪" << endl;
    }
};

int main(void) {
    //第一种情况 父子类之间的类型转换
    Dog* dog1 = new Dog();
    Animal* a1 = static_cast<Animal*>(dog1); //子类的指针转型到父类指针,用隐式转换也可,即 Animal* a1 = dog1
    
    Dog* dog1_1 = static_cast<Dog*>(a1); //父类的指针转型到子类的指针
    Cat* cat1 = static_cast<Cat*>(a1);  //一个子类到另一个子类,有风险,这样时不行的,会出问题
    
    Dog dog2;
    Animal& a2 = static_cast<Animal&>(dog2); //子类的引用转型到父类的引用
    Dog &dog2_2 = static_cast<Dog&>(a2); //父类到子类引用
    
    //第二种 基本类型的转换(存在数据丢失问题,需要注意)
    int kk = 234;
    char cc = static_cast<char>(kk);
    
    //第三种 把空指针转换成目标类型的空指针。
    int* p = static_cast<int*>(NULL);           //int *p = NULL隐式转换也可以
    Dog* dp = static_cast<Dog*>(NULL);
    
    //第四种 把任何类型的表达式转换成 void 类型
    int* pi = new int[10];
    void* vp = static_cast<void*>(pi);
    vp = pi;
    system("pause");
    return 0;
}
(2)reinterpreter_cast

重新解释类型(挂羊头,卖狗肉) 不同类型间的互转,数值与指针间的互转

用法:

TYPE b = reinterpret_cast <TYPE> (a)

TYPE 必须是一个指针、引用、算术类型、函数指针.

注意:滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。

#include <iostream>

using namespace std;

class Animal {
public:
	void cry() {
		cout << "动物叫" << endl;
	};
};

class Cat : public Animal
{
public:
	void cry()
    {	
    	cout << "喵喵瞄" << endl;
    }
};

class Dog : public Animal
{
public:
    void cry()
    {
    	cout << "汪汪汪" << endl;
    }
};

int main02(void) {
    //用法一 数值与指针之间的转换
    int* p = reinterpret_cast<int*>(0x99999);
    int val = reinterpret_cast<int>(p);

    //用法二 不同类型指针和引用之间的转换
    Dog dog1;
    Animal* a1 = &dog1;  //子类地址能赋给父类指针,但子类指针不允许指向父类
    a1->cry();
    
    Dog* dog1_p = reinterpret_cast<Dog*>(a1);
    Dog* dog2_p = static_cast<Dog*>(a1); //如果能用 static_cast ,static_cast 优先
    
    //Cat* cat1_p = static_cast<Cat*>(a1);这里还是因为父子类之间的指针可以相互转换
    //Cat* cat2_p = static_cast<Cat*>(dog1_p);//NO! 不同类型指针转换不能使用 static_cast
    Cat* cat2_p = reinterpret_cast<Cat*>(dog1_p);
    
    Animal& a2 = dog1;
    Dog& dog3 = reinterpret_cast<Dog&>(a2);//引用强转用法
    
    dog1_p->cry();
    dog2_p->cry();
    
    cat2_p->cry();
    
    system("pause");
    return 0;
}
(3)dynamic_cast

动态类型转换,主要用在继承中,在多态的特性下使用

  • 将一个基类对象指针 cast 到继承类指针,dynamic_cast 会根据基类指针是否真正指向继承类指针来做相应处理。失败返回 null,成功返回正常 cast后的对象指针;
  • 将一个基类对象引用 cast 继承类对象,dynamic_cast 会根据基类对象是否真正属于继承类来做相应处理。失败抛出异常 bad_cast

注意:dynamic_cast 在将父类 cast 到子类时,父类必须要有虚函数一起玩。

#include <iostream>

using namespace std;

class Animal {
public:
	virtual void cry() = 0;
};

class Cat : public Animal
{
public:
	void cry()
    {	
    	cout << "喵喵瞄" << endl;
    }
    
    void play()
    {
    	cout << "爬爬树"<<endl;
    }
};

class Dog : public Animal
{
public:
    void cry()
    {
    	cout << "汪汪汪" << endl;
    }
    
    void play()
    {
    	cout << "溜达溜达" << endl;
    }
};

void animalPlay(Animal& animal) {
	animal.cry();
    
    try {
        Dog& pDog = dynamic_cast<Dog&>(animal);
        pDog.play();
    }
    catch (std::bad_cast bc) {
    	cout << "不是狗,那应该是猫" << endl;
    }
    
    try {
        Cat& pCat = dynamic_cast<Cat&>(animal);
        pCat.play();
    }
    catch (std::bad_cast bc) {
    	cout << "不是猫,那应该是上面的狗" << endl;
    }
}

void animalPlay(Animal* animal) {
    animal->cry();
    
    Dog* pDog = dynamic_cast<Dog*>(animal);
    if (pDog) {
    	pDog->play();
    }
    else {//pDog == NULL
    	cout << "不是狗,别骗我!" << endl;
    }
    
    Cat* pCat = dynamic_cast<Cat*>(animal);
    if (pCat) {
    	pCat->play();
    }
    else {//pDog == NULL
    	cout << "不是猫,别骗我!" << endl;
    }
}

int main(void) {
    Dog* dog1 = new Dog();
    Animal* a1 = dog1;
    
    //animalPlay(a1);
    
    Dog dog2;
    animalPlay(dog2);
    
    Cat* cat1 = new Cat();
    Animal* a2 = cat1;
    
    //animalPlay(a2);
    Cat cat2;
    animalPlay(cat2);
    
    system("pause");
    return 0;
}
(4)const_cast

去 const 属性。(仅针对于指针和引用)

#include <iostream>
using namespace std;
void demo(const char* p)
{
    //对指针去掉 cost 重新赋值
    //char* p1 = const_cast<char *>(p);
    //p1[0] = 'A';
    
    //直接去掉 const 修改
    const_cast<char*>(p)[0] = 'A';
    
    cout << p << endl;
}

void demo(const int p)
{
    int q = p;
    //const_cast<int>(p) = 888;// NO ! 不能对非指针和引用进行 const 转换
    cout << p << endl;
}

int main(void)
{
    //字符串数组
    //char p[] = "12345678";
    //demo(p); //合情合理
    
    //常量字符串不能去掉 const 修改
    //警告: 在去掉常量限定符之前,保证指针所指向的内存能够修改,不能修改则会引起异常。
    const char* cp = "987654321";
    demo(cp);
    
    system("pause");
    return 0;
}

总结:

  • static_cast 静态类型转换,编译的时 c++编译器会做编译时的类型检查;隐式转换;基本类型转换,父子类之间合理转换
  • 若不同类型之间,进行强制类型转换,用 reinterpret_cast<>() 进行重新解释
  • 建议:C 语言中 能隐式类型转换的,在 c++中可用 static_cast<>()进行类型转换。因 C++编译器在编译检查一般都能通过;C 语言中不能隐式类型转换的,在 c++中可以用reinterpret_cast<>() 进行强制类型解释。
  • static_cast<>()和 reinterpret_cast<>() 基本上把 C 语言中的强制类型转换给覆盖,但是注意 reinterpret_cast<>()很难保证移植性。
  • dynamic_cast<>(),动态类型转换,安全的虚基类和子类之间转换;运行时类型检查
  • const_cast<>(),去除变量的只读属性

最后的忠告:程序员必须清楚的知道: 要转的变量,类型转换前是什么类型,类型转换后是什么类型,转换后有什么后果。

C++大牛建议:一般情况下,不建议进行类型转换;避免进行类型转换。

🚀五、左值、右值、左值引用、右值引用

在这里插入图片描述

⛳(一)左值和右值的概念

按字面意思,通俗地说。以赋值符号 = 为界,= 左边的就是左值(lvalue),= 右边就是右值(rvalue)。

int a = 666;
 //左值 右值
int b = 888;
 //左值 右值
int c = a + b;
 //左值 右值 右值

lvalue - 代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。

rvalue - 通过排他性来定义,每个表达式不是 lvalue 就是 rvalue。因此从上面的lvalue 的定义,rvalue 是在不在内存中占有确定位置的表达式,而是存在在寄存器中。

所有的左值(无论是数组,函数或不完全类型)都可以转换成右值(从内存移到寄存器中,涉及到汇编)

  • 不能修改的左值也是右值!(就比如常量,包括常数以及const修饰的变量)
  • 不能作为左值的值就是右值!
  • 左值有时候也可当做是右值来使用!
  • C++中的任何一条表达式,要么是左值,要么就是右值,不可能是其他值了!
  • (一个左值,可能同时具有左值属性和右值属性。也即左值是有右值属性的,但本质上还是左值)(一个右值,只具有右值属性。不可能同时具有左值属性和右值属性。)

左值是什么呢?左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。

⛳(二)左值引用和右值引用

①左值引用(绑定到左值)

int value = 10;
int& refval = value;
refval = 13;//==> value =13;

const int& c = 1;//将const常量引用c绑定到1上!

左值引用一般是不能绑定右值,除非你用const的左值引用,其实就算const常量引用了

右值引用(绑定到右值):是C++11的新标准中引入的概念!用两个引用符号&&来do!

//这是C98写的左值引用和常量引用的常见用法:
int a = 1;
int& b = a;//√正确!可将左值引用绑定到左值上!b == 1
int& b = 1;//错误!非常量引用的表达式必须为左值!
//也即无法将左值引用变量b绑定到右值1上!
 
//这是C++11新引入的右值引用的用法:
int aa = 11;
int&& bb = aa;//×错误!无法将右值引用绑定到左值!
//也即无法将右值引用变量bb绑定到aa这个左值上!
int&& bb = 11;//√正确!可将右值引用绑定到右值上! bb == 11

注意:

当右值引用和模板结合的时候,T&& 并不一定表示右值引用,它可能是个左值引用又可能是个右值引用 (这里的 && 是一个未定义的引用类型,称为通用引用universal references ),它必须被初始化,它是左值引用还是右值引用却决于它的初始化:

  • 如果它被一个左值初始化,它就是一个左值引用;
  • 如果被一个右值初始化,它就是一个右值引用。

🚀六、数组的替代品

模板类vector:

模板类vector类似于string类,也是一种动态数组。可以在运行阶段设置vector对象的长度,可在末尾附加新数据,还可在中间插入新数据。基本上,它是使用new创建动态数组的替代品。实际上,vector类确实使用new和delete来管理内存,但这种工作是自动完成的。

首先,要使用vector对象,必须包含头文件vector。其次,vector包含在名称空间std中,因此您可使用using编译指令、using声明或std::vector。第三,模板使用不同的语法来指出它存储的数据类型。第四,vector类使用不同的语法来指定元素数。

#include <vector>
...
using namespace std;
vector<int> vi ;      //create a zero-size array of int
int n;
cin >> n;
vector<double> vd(n); // create an array of n doubles

模板类array(c++11):

vector类的功能比数组强大,但付出的代价是效率稍低。如果您需要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便和安全。有鉴于此,C++11新增了模板类array,它也位于名称空间std中。与数组一样,array对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同,但更方便,更安全。要创建array对象,需要包含头文件array。array对象的创建语法与vector稍有不同:

推而广之,下面的声明创建一个名为arr的array对象,它包含n_elem个类型为typename的元素:

array<typeName, n_elem> arr;

区别:

首先,注意到无论是数组、vector对象还是array对象,都可使用标准数组表示法来访问各个元素。其次,从地址可知,array对象和数组存储在相同的内存区域(即栈)中,而vector对象存储在另一个区域(自由存储区或堆)中。第三,注意到可以将一个array对象赋给另一个array对象;而对于数组,必须逐元素复制数据。

🚀七、其它

int main(void)

在括号中使用关键字void明确地指出,函数不接受任何参数。在C++(不是C)中,让括号空着与在括号中使用void等效,在C中,让括号空着意味着对是否接受参数保持沉默——这意味着将在后面定义参数列表。在C++中,不指定参数列表时应使用省略号:

void sayBye(...);

通常,仅当与接受可变参数的C函数(如printf( ))交互时才需要这样做


C++风格注释为//,可以在程序中使用C或C++风格的注释,C风格有/* */和//两种

C++对于变量名称的长度没有限制,但是有些平台有长度限制,C只保证名称中的前63个字符有意义

C++允许您针对类定义递增递减运算符,在这种情况下,用户这样定义前缀函数:将值加1,然后返回结果;但后缀版本首先复制一个副本,将其加1,然后将复制的副本返回。因此,对于类而言,前缀版本的效率比后缀版本高。


基本整型:char、short、int、long、long long(c++11),其中每种类型都有符号版本和无符号版本

short至少16位;
int至少与short一样长;
long至少32位,且至少与int一样长;
long long至少64位,且至少与long一样长。

两字节的int可能是16位,而在另一个系统中可能是32位,字节的含义依赖于实现

如果没有非常有说服力的理由来选择其他类型,则应使用int。如果知道变量可能表示的整数值大于16位整数的最大可能值,则使用long。即使系统上int为32位,也应这样做。这样,将程序移植到16位系统时,就不会突然无法正常工作。如果要存储的值超过20亿,可使用long long。

如果short比int小,则使用short可以节省内存。通常,仅当有大型整型数组时,才有必要使用short。(数组是一种数据结构,在内存中连续存储同类型的多个值。)如果节省内存很重要,则应使用short而不是使用int,即使它们的长度是一样的。例如,假设要将程序从int为16位的系统移到int为32位的系统,则用于存储int数组的内存量将加倍,但short数组不受影响。请记住,节省一点就是赢得一点。

这些整型变量的行为就像里程表。如果超越了限制,其值将为范围另一端的取值(参见图3.1)。C++确保了无符号类型的这种行为;但C++并不保证符号整型超越限制(上溢和下溢)时不出错,而这正是当前实现中最为常见的行为。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzXld9An-1687358530827)(E:\create\图片\C++\1\3.png)]

8位char可以表示基本字符集,另一种类型wchar_t(宽字符类型)可以表示扩展字符集。wchar_t类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。

cin和cout将输入和输出看作是char流,因此不适于用来处理wchar_t类型。iostream头文件的最新版本提供了作用相似的工具—wcin和wcout,可用于处理wchar_t流。另外,可以通过加上前缀L来指示宽字符常量和宽字符串。

wchar_t bob = L'P';    // a wide-character constant
wcout << L"tall" << endl; // outputting a wide-character string

如果有特定长度和符号特征的类型,将很有帮助,而类型wchar_t的长度和符号特征随实现而已。因此,C++11新增了类型char16_t和char32_t,其中前者是无符号的,长16位,而后者也是无符号的,但长32位。C++11使用前缀u表示char16_t字符常量和字符串常量,如u‘C’和u“be good”;并使用前缀U表示char32_t常量,如U‘R’和U“dirty rat”

整形常量存为int,除非使用了特殊的后缀或者值太大不能存储为int:

后缀是放在数字常量后面的字母,用于表示类型。整数后面的l或L后缀表示该整数为long常量,u或U后缀表示unsigned int常量,ul(可以采用任何一种顺序,大写小写均可)表示unsigned long常量(由于小写l看上去像1,因此应使用大写L作后缀)。例如,在int为16位、long为32位的系统上,数字22022被存储为int,占16位,数字22022L被存储为long,占32位。同样,22022LU和22022UL都被存储为unsigned long。C++11提供了用于表示类型long long的后缀ll和LL,还提供了用于表示类型unsigned long long的后缀ull、Ull、uLL和ULL。

接下来考察长度。在C++中,对十进制整数采用的规则,与十六进制和八进制稍微有些不同。对于不带后缀的十进制整数,将使用下面几种类型中能够存储该数的最小类型来表示:int、long或long long。在int为16位、long为32位的计算机系统上,20000被表示为int类型,40000被表示为long类型,3000000000被表示为long long类型。对于不带后缀的十六进制或八进制整数,将使用下面几种类型中能够存储该数的最小类型来表示:int、unsigned int long、unsigned long、long long或unsigned long long。在将40000表示为long的计算机系统中,十六进制数0x9C40(40000)将被表示为unsigned int。这是因为十六进制常用来表示内存地址,而内存地址是没有符号的,因此,usigned int比long更适合用来表示16位的地址。

有趣的是,程序中输入的是M,而不是对应的字符编码77。另外,程序将打印M,而不是77。通过查看内存可以知道,77是存储在变量ch中的值。这种神奇的力量不是来自char类型,而是来自cin和cout,这些工具为您完成了转换工作。输入时,cin将键盘输入的M转换为77;输出时,cout将值77转换为所显示的字符M;cin和cout的行为都是由变量类型引导的。如果将77存储在int变量中,则cout将把它显示为77(也就是说,cout显示两个字符7)。


行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈七.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值