再论C++

1、C++的引用

变量名,本身是一段内存的引用,即别名(alias)。此处引入的引用,是为己有变量起一个别名。
声明如下:

int a;
int &ra = a;

引用规则

1、引用,是一种关系型声明,而非定义。不能独立存在,必须初始化,且与原类型保持一致,且不分配内存。
2、声明关系,一经声明,不可变更。
3、可对引用,再次引用。多次引用的结果,是某一变量具有多个别名,多个别名间是平行关系。
4、辨别引用与其它,&符号前有数据类型时,是引用,其它皆为取地址或按位与。

引用的真正目的,就是取代指针传参。C++引入引用后,可以用引用解决的问题。避免用指针来解决。

引用扩展了变量的作用域,传参后,就像在本地解决问题一样。避免了传 n 级指针,解决 n-1 级指针的问题,即平级内解决问题。

引用的本质

引用的本质是 const 类型的指针,即 type * const ref。其功能类似指针,看起来又不是指针,是对指针的封装。

探讨指针的引用与引用的指针

#include <iostream>
using namespace std;
int main()
{
    int *p;
    int * & rp = p;   //正确,指针的引用
    int & * prp =&rp; //cannot declare pointer to 'int&' 错误,引用的指针
    return 0;
}

结论:指针的引用可以有,引用的指针不存在

探讨指针的指针与引用的引用

#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
    int *p;
    int **pp=&p; //可以为引用再次声明引用,但不可建立引用的引用。避免了指针调计的"失误"
    int a;
    int & rx = a;
    int & ry = a;
    int & rz = ry;   //这是对引用的再次引用,可以
    int && rrz = rz; //引用的引用,是不存在的,C++11 赋于它新的意义,右值引用
    return 0;
}

结论:指针的指针可以有,引用的引用不存在

探讨指针数组与引用的数组

#include <iostream>
using namespace std;
int main()
{
    int a, b, c;
    int * pArr[] = {&a,&b,&c};
    int & rArr[] = {a,b,c}; //err  int & * 引用的指针不存在,这个肯定也不存在
    return 0;
}

结论:指针数组有,引用数组没有

探讨数组的引用

//pr和ra都是array的引用
int main()
{
    int array[5];
     cout << "sizeof(array)" << sizeof(array) << endl;   //20
    //数组名的两重性
    int * const & pr = array;       //int * &,这里必须加const且只能放在这个位置
                                    //(因为array是常量,不可以修改,所以加const修饰),不然报错
                                    //这里array用的是数组名代表首元素的地址array为int*类型,
                                 //这里C++中不允许pr可以改变,从而改变了数组,要写成const
    cout << "sizeof(pr)" << sizeof(pr) << endl;      //8
    
    int (&ra)[5] = array;            //数组的引用,array在这里是代表数组整体
    cout << "sizeof(ra)" << sizeof(ra) << endl;     //20
    
    return 0;
}

C++中的const与常引用(const&)

在C++中的const才是真正的const,C语言中const修饰的变量,可以通过指针去修改,但是在C++中的const是不可以的,是真正的const。在C++中都是使用const来代替#define宏定义的。

要注意的是,C++中使用const的变量,一定要在定义的时候初始化,不然会报错。

常引用

const 的本意, 即不可修改。 所以, const 对象, 只能声明为 const 引用, 使其语义保持一致性。 非const 对象, 既可以声明为 const 引用, 也可以声明为非const 引用。声明为 const 引用, 则不可以通过 const 引用修改数据

#include <iostream>
using namespace std;
int main()
{
    const int val = 10;
    //int & rv = val; 这是错误的
    const int & rv2 = val; //注意const的位置,可以在int前后,&之前
    
    int data;
    int &rd = data;
    const int &rd2 = data;
    return 0;
}

临时对象的常引用

临时对象, 通常理解为不可以取地址的对象, 比如函数的返回值, 即 CPU 中计算产生的中间变量通常称为右值。 常见临时值有常量, 表达式等。

#include <iostream>
using namespace std;
//临时变量 即不可取地址的对象
//常量 表达式 函数返回值 类型不同的变量
int foo()
{
    int a = 100;
    return a;
}
int main()
{
    //常量
    const int & cc = 55;
    cout<<cc<<endl;
    
    //表达式
    int a = 3; int b = 5;
    const int &ret = a+b;
    
    //函数返回值
    const int& ra = foo();
    
    //类型不同的变量
    double d = 100.12;
    const int &rd = d;
    return 0;
}

本质上 const 引用, 引用了一个不可改变的临时变量,const int tmp = data; c onst int &rd = tmp;此时, 我们改变了 data 的值, 临时变量的值也并没有发生改变。

const type & (存在)与 type &const (不存在)这两个修饰方式,此处可以参考 const 修饰指针的用法, int * const p 的话, 表示 p 不可更改指向, 而引用呢, 本身一经声明, 即不可更改指向, 故也不存在这个修饰方法了。意思就是比如&ra = a;了,那么ra就一直指向a了,不可以更改的。

2、堆内存操作

C 语言中提供了 malloc 和 free 两个系统级别的函数, 完成对堆内存的申请和释放。

而 C++则提供了两关键字 new/new[]和 delete(delete[]), 此两关健字, 不仅可以提供堆内存的申请和释放, 还有其它的用途, 主要是为了 C++面向对象而扩展的功能 。

记住new/new[]和 delete(delete[])都是关键字,不是函数。

new/new[]

申请单变量空间

new 后面直接跟类型即可, 比如 new int, 申请一个 int 类型空间的大小, int int(10)后面括号里面跟的是初始化的值 。

int main()
{
    int *p = new int(10); //new   相当于   int *p = (int*)malloc(sizeof(int));
    *p = 100;
    cout<<*p<<endl;
    int **pp = new int *; // (int**)malloc(sizeof(int*))
    Stu *ps = new Stu;
    return 0;
}

需要注意的是,new失败的是异常。

申请一或多维数组

new 关键字, 后面跟上类型和维度, 比如申请一个 10 个 int 类型大小的数组, 即 new int[10], 后面也可以跟初始化数据。

下面分别申请了一维数组,指针数组,二维数组,三维数组的例子

int main()
{
    float *p = new float [10]{1.2,3.4};// new float [10] malloc(10*sizeof(float);
                                       //申请了float类型的数组,并初始化
    char **pp = new char *[11];           //申请了char*类型的数组(指针数组)
    for(int i=0; i<10; i++)
    {
        pp[i] = "China";
    } 
    pp[10] = nullptr;
    while(*pp)
    {
        cout<<*pp++<<endl;
    }
    
    
    int(*ppp)[5] = new int[3][5];            //申请了二维数组的空间
    int (*pppp)[3][5] = new int[2][3][5];        //申请了三维数组的空间
    
    
}

释放空间 delete 和 delete[]

对于申请的单变量空间, 采用 delete 后面跟指针即可, 比如 delete p; 对于一维或是多维数组一律采用 delete[] 。

int main()
{
    int *p = new int;
    delete p;                        //释放单变量
    
    int **pp = new int*[10];
    delete []pp;                    //释放指针数组
    
    int (*ppp)[5] = new int[3][5];
    delete []ppp;                    //释放二维数组
}

对于 malloc 申请出的空间, 类型是 void*型, 如果申请失败, 则返回 NULL, 可以能过返值对申请成功与否作过判断。

new 申请空间失败不是返回 NULL,而是抛出异常, 应当采用 try catch 结构对其处理。 当然也我们可以采用不抛出异常的方式, 此时用法同 malloc。

使用规则
1 、new/delete 是关键字, 效率高于 malloc 和 free。
2 、配对使用, 避免内存泄漏和多重释放。
3 、避免, 交叉使用。 比如 malloc 申请的空间去 delete, new 出的空间被 free。

此两关键字, 重点用在类对象的申请与释放。 申请的时候会调用构造器完成初始化,释放的时候, 会调用析构器完成内存的清理。 等后面说道类的时候,再提new和delete。

3、C++的类型强制转化

使用 C 风格的强制转换可以把想要的任何东西转换成合乎心意的类型。但是新类型的强制转换可以提供更好的控制强制转换过程, 允许控制各种不同种类的强制转换。 C++提供了四种转化 static_castreinterpret_castdynamic_castconst_cast 以满足不同需求, C++风格的强制转换好处是, 它们能更清晰的表明它们要干什么。 C 语言转换风格, 在 C++中依然适用。

static_cast

语法格式static_cast<type>(expression)
适用场景在一个方向上可以作隐式转换, 在另外一个方向上就可以作静态转换。 双向可隐式的自然不需要强制转换。
#include <iostream>
using namespace std;
int main()
{
    //双向隐式
    int a = 3;
    float b = 5.5;
    
    a = static_cast<int>(b) ;
    b = static_cast<float>(a);
    
    int x =10; int y = 3;
    float z = static_cast<float>(x)/y;
    cout<<z<<endl;
    
    //单向隐式
    void *p; int *q;
    //q = p; //err
    p = q;
    q = static_cast<int*>(p);
    char *pp = static_cast<char*>(malloc(100));
    return 0;
}

reinterpret_cast

语法格式reinterpret_cast<type>(expression)
适用场景"通常为操作数的位模式提供较低层的重新解释"也就是说将数据以二 进制存在形式的重新解释, 在双方向上都不可以隐式类型转换的, 则 需要重解释类型转换。
int main()
{
    int *p; float *q;
    //p = q; //err
    //q = p; //err
    //p = static_cast<int*>(q); //err
    //q = static_cast<int*>(p); //err
    p = reinterpret_cast<int*>(q);
    q = reinterpret_cast<float*>(p);

    return 0;
}

const_cast

语法格式const_cast<type>(expression)
适用场景用 来 移 除 非 const 对 象 的 引 用 或 指 针 的 常 量 性 (cast away the constness)使用 const_cast 去除 const 对于引用和指针限定, 通常是 为了函数能够接受参数或返回值。 非 const 对象--> const 引用或指针-->脱 const-->修改非 const 对象
评价被 const 引用后的, 返作用。
注意目标类类型只能是指针或引用,否则报错

原生数据是非 const 的, 可以去除其引用的 const 的属性, 若原生数据是 const 的,去除其引用的 const 属性, 执行任何写入操作都是未定义的。

#include <iostream>
using namespace std;
//const_cast 只作用于指针和引用,去 const 化
//const_cast 理解为, const semantic 的补充
//非 const 对象-->const 引用/指针->去 const->原非 const 对象修改
void foo(const int &a)
{
    const_cast<int&>(a) = 200;
} 
int main()
{
    int a;
    const int & ra = a;
    a = 100;
    cout<<a<<endl;
    const_cast<int&>(ra) = 300;
    cout<<ra<<endl;
    cout<<a<<endl;
    
    
    const int *p = &a;
    *const_cast<int*>(p)= 400;
    cout<<*p<<endl;
    foo(a);
    return 0;
}

dynamic_cast

用于多态中的父子类之间的强制转化, 以后再讲。

4、命名空间

命名空间为了大型项目开发, 而引入的一种避免命名冲突的一种机制。 比如, 在一个大型项目中, 要用到多家软件开发商提供的类库。 在事先没有约定的情况下, 两套类库可能存在同名的函数或是全局变量而产生冲突。 项目越大, 用到的类库越多, 开发人员越多,这种冲突就会越明显。鉴于这类大型项目的开发需要, C++引入的了命名空间的概念。

namespace 定义
语法 C++引入 namespace 的本质是对全局命名空间再次划分。 确切的说, 是对全局的函数和变量再次进行作用域打包。

命名空间的声明及 namespace 中可以包含的内容。

namespace spacename {
    global varialbe;  //全局变量
    function;          //函数
    struct type;      //结构体类型
    other namespace;  //其他命名空间
}

常见的命名空间有三种层次的用法, 示例如下: 假设有空间空间

namespace sn {};

方法举例备注
sn::member;sn::a = 5; sn::foo();直接使用命名空间成员, 不会产生命名冲突。
using sn::memberusing sn::a; using sn::foo();命名空间成员, 融入该语句所在的命令空间, 可以会产生冲突。
using namespace sn;using namesapce std;将命名空间(即全体成员), 融 入该语句所在的命名空间。可能会产生命令空间。
using namespace one::other;using namespace one::other;命名空间支持嵌套。

标准命名空间 std::
std 是 C++标准库的命名空间,这就是在学习 C++一开始的时候, using namespace std的原故。 当然了, 前述的几种使用方法依然适用。但是, 由于标准库定义的方法, 大家多是耳熟能详, 所以一般不会跟标准库产生命名
冲突, 所以才有了这种偷懒的写法, using namespace std; 。

协同开发

相同的命名空间会自动合并,比如我在a.h中定义了namesapce one { },我又在b.h中定义了namespace one { },那么这两个命名空间会自动合并,所以我们在实际工作中,才可以协同开发,使用相同的命名空间会自动合并。.h和.cpp中都要用namespace包含起来。

5、string类

string 是 C++中处理字符串的类, 是对 C 语言中字符串的数据和行为的包装。 使对字符串的处理, 更简单易用。
使用string的时候要包含头文件#include <string> ,有时候不包含也能用是因为在iostream中包含的头文件中层层包含了string这个头文件。(我找过,找到了)

定义一个字符串的时候,这样定义即可string str = "China";
注意:在我们用cin输入string类型的时候,遇到空格之后,空格后面的东西会消失,不会算作string的内容,我们想要解决这个问题,可以使用getline(cin,s)这里的s是string类型的。

string 重载了运算符+ < > = != += , 使字符串的操作变的非常简单。 其运算方式类似于普通数值运算, 这是运算符重载的好处。 运算符重载我们过后讲。

#include <iostream>
#include <string.h>
using namespace std;
int main()
{
    string s1 = "China";
    string s2 = "China";
    if(s1 < s2) 
    {
        cout<<"s1 < s2"<<endl;
    }  
    else if(s1 > s2)
    {
        cout<<"s1 > s2"<<endl;
    } 
    else
    {
        cout<<"s1 == s2"<<endl;
    } 
    s1 += s2;
    cout<<"s1 ="<<s1<<endl;
    return 0;
}    

数值与字符串互转函数(全局函数,C++11新增)

value to string

string to_string(int val);
string to_string(unsigned val);
string to_string(unsigned long val);
string to_string(long long val);
string to_string(unsigned long long val);
string to_string(float val);
string to_string(double val);
string to_string(long double val);
#include <iostream>
using namespace std;
int main()
{
    int a = 1234;
    string str = to_string(a);
    str += "5678";
    cout<<str<<endl;
    return 0;
}

string to value

int stoi(const string& str, size_t *idx=0, int base=10);
long stol(const string& str, size_t *idx=0, int base=10);
unsigned long stoul(const string& str, size_t *idx=0, int base=10);
long long stoll(const string& str, size_t *idx=0, int base=10);
unsigned long long stoull(const string& str, size_t *idx=0, int base=10);
float stof(const string& str, size_t *idx=0);
double stod(const string& str, size_t *idx=0);
long double stold(const string& str, size_t *idx=0);
#include <iostream>
using namespace std;
int main()
{
    string str = "1111a";

    int a = stoi(str);
    a++;
    cout<<a<<endl;
    string strbin = "-10010110001";
    string strhex= "0x7f";
    int bin = stoi(strbin,nullptr,2);
    int hex = stoi(strhex,nullptr,16);
    cout<<"bin:"<<bin<<endl;
    cout<<"hex:"<<hex<<endl;
    return 0;
}

string数组

string 数组在 C 语言中要存储如下这样的数据, 要用到二维空间, 要进行两个层次的空间申请与释放但在 C++中就不需要了, 并且一样是高效的。

1
22
333
4444
55555
666666
7777777
88888888
999999999
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
    string strArray[10] = {
        "0",
        "1",
        "22",
        "333",
        "4444",
        "55555",
        "666666",
        "7777777",
        "88888888",
        "999999999",
    };
    for(int i=0; i<10; i++)
    {
        cout<<strArray[i]<<endl;
    }
    return 0;
}

Bjarne Stroustrup's Advice

(一) 在 C++中几乎不需要用宏, 用 const 或 enum 定义显式的常量, 用 inline 避免函数调用的额外开销, 用模板去刻画一族函数或类型, 用 namespace 去避免命名冲突。
(二) 不要在你需要变量之前去声明, 以保证你能立即对它进行初始化。
(三) 不要用 malloc, new 运算会做的更好。
(四) 避免使用 void*、 指针算术、 联合和强制, 大多数情况下, 强制都是设计错误的指示器。
(五) 尽量少用数组和 C 风格的字符串, 标准库中的 string 和 vector 可以简化程序。
(六) 更加重要的是, 试着将程序考虑为一组由类和对象表示的相互作用的概念, 而不是一堆数据结构和一些可以拨弄的二进制。

Last modification:December 3rd, 2019 at 07:30 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment