欢迎光临
我们一直在努力

【C++】 入门篇 从C到C++

前言

在常见的C++代码中我们经常能够看到开头总是有

#include <iostream>
using namespace std;
int main()
{
return 0;
}

这些是什么东西?干什么用的?和C语言有什么区别呢,看完这一篇你就从C语言成功转入C++了。

一、命名空间

1.1引入

在C语言中我们要定义一些变量可能会出现与库中的一些函数和变量一致时会名字冲突导致我们无法进行定义比如:

#include <stdio.h>
// 定义和库函数同名的全局函数
void printf(const char* str)
{
printf("我的printf");
}
int main()
{
printf("test");
return 0;
}

1.2使用命名空间

但是C++就可以解决C的命名冲突问题
它的关键字就是

namespace

先说一下命名空间是如何进行定义的,命名空间的定义分为3种:

  • 命名空间名称及域作用限定符

– 域作用限定符

访问对象只在全局域中出现直接访问全局域
访问对象在全局域和函数局部域同时出现 就近访问原则,优先访问局部
使用域访问限定符时 直接访问域里面的

在项目中我们经常使用这种因为不同的人取的名可能会重复。在上面那个例子中我们就可以这么写。

// 自定义命名空间
namespace ljm
{
void printf(const char* str)
{
// 前面加::是为了调用c库的printf
::printf("我的printf:%s\\n", str);
}
}
int main()
{
// 调用自定义命名空间的printf
ljm::printf("test");
return 0;
}

在这里插入图片描述

  • 使用using namespace 命名空间名称 引入

我们在日常运用中经常使用部分的函数,会把命名空间的所有变量函数都展开,这样就可以减少我们的代码量,不然每一次使用一个东西都要引入他的命令空间这也就解释了为什么很多代码中都有

using namespace std;

这里就是把std命令空间的东西进行展开

  • 举个例子

命名空间N里面有变量A,B,C,D。我们就可以通过这样来引入

//这样就引入了N里的所有变量函数
namespace N {
int A = 1;
int B = 2;
int C = 3;
int D = 4;
}

using namespace N;
int main()
{
printf("%d", A);
printf("%d", B);
printf("%d", C);
printf("%d", D);
return 0;
}

在这里插入图片描述

– 使用using将命名空间中某个成员引入

但是假设我们如果不需要N里面的所有变量只要A呢我们就可以这样

namespace N {
int A = 1;
int B = 2;
int C = 3;
int D = 4;
}

using N::A;
int main()
{
printf("%d\\n", A);
return 0;
}

在这里插入图片描述

二、C++输入&输出

2.1引入

学习任何语言之前我们都要学习他的输入输出。

2.2使用

#include<iostream>
// std是C++标准库的命名空间名,C++将输入输出等定义实现都放到这个命名空间中
using namespace std;
int main()
{
int a = 0;
cin>>a;
cout<<"Hello C++!!!"<<endl;
return 0;
}

2.3区别

– 在C语言中我们的输入输出都是在<stdio.h>的printf,scanf。
– 在C++中我们的输入输出是在<iostream>的cout,cin。
– 细心的你肯定发现了c++的头文件没有.h对的我们在c++包含头文件都不要.h

那如果我们要c语言中的头文件呢我们就可以这样写这样就包含了c的输入输出头文件。
– 在c++中我们们不用像写c一样去写类型了为什么呢?这个后面揭晓。

2.4收尾

  • c++的输入输出更像一种流。
  • cin:会从标准输入流读到东西流到变量里。
  • cout:会从标准变量读到东西流到标准输出流里。

三、缺省参数

3.1 概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。有点像备胎的意思了。
大概这样的。

void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func();//在没有传实参是会用0
Func(10);//有传实参的时候就不用,用自己的。
return 0;
}

3.2分类

  • 全缺省参数

void Func(int a = 10, int b = 20, int c = 30)
{
//每个值都有缺省参数
cout<<"a = "<<a<<endl;//输出10
cout<<"b = "<<b<<endl;//输出20
cout<<"c = "<<c<<endl;//输出30
}

  • 半缺省参数

void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;//a必须传值,他没有缺省参数,不然会报错
cout<<"b = "<<b<<endl;//10
cout<<"c = "<<c<<endl;//20
}

注意:

1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现
3. 缺省值必须是常量或者全局变量(如果给一个变量编译器不知道是什么值)

//a.h
void Func(int a = 10);

// a.cpp
void Func(int a = 20)
{}

// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,
//那编译器就无法确定到底该用那个缺省值。

四、 函数重载

4.1引入

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。在c中我们要实现add进行不同类型要取不同的名称,比较繁琐.

static inline int add_i(int a, int b)
{
return a + b;
}
double add_d(double a, double b)
{
return a + b;
}

4.2 概念

是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同来解决不同的问题。

  • 参数类型不同

像刚才那种情况我们就可以这样写。

int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
cout<< add(1, 1) << endl;
cout << add(1.1, 1.1) << endl;
return 0;
}

函数调用就像这样的编译器它会自动处理
在这里插入图片描述

– **参数类型顺序不同**

void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}

– **参数个数不同**

void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}

4.3 C++支持函数重载的原理–名字修饰

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:
预处理编译汇编链接
四个阶段,最终生成可执行文件。C++和C基本都一样但是在链接处有点区别就导致了C++支持重载
在这里插入图片描述

  • 实际项目通常是由多个头文件和多个源文件构成,我们知道,当前 a.cpp 中调用了 b.cpp 中定义的 Add 函数时,编译后链接前,a.o 的目标文件中没有 Add 的函数地址,因为 Add 是在 b.cpp 中定义的,所以 Add 的地址在 b.o 中。那么怎么办呢?

  • 所以链接阶段就是专门处理这种问题,链接器看到 a.o 调用 Add,但是没有 Add 的地址,就会到 b.o 的符号表中找 Add 的地址,然后链接到一起。符号表就是一个函数对应一个所处地址。

  • 那么链接时,面对 Add 函数,链接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。

  • 在 Linux 下 g++ 的修饰规则简单易懂,下面我们使用了 g++ 演示了这个修饰后的名字。

    在这里插入图片描述

    这里发现在linux下,C语言采用gcc编译完成后,函数名字的修饰没有发生改变
    在这里插入图片描述
    这里发现在linux下,C++语言采用g++编译完成后,函数名字的修饰发生了改变会在后面加入类型,长度

  • 通过上面我们可以得出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
  • 注意:
    如果两个函数函数名和参数是一样的,返回值不同是不构成重载的因为调用时编译器没办法区分,因为函数的函数名修饰规则没有包含返回值.

    五、引用

    5.1 概念

    引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。有点像共享单车。我和你都可以使用这个共享单车。

    5.2使用

    类型& 引用变量名(对象名) = 引用实体;

    int a=0;
    int& ra = a;//定义引用类型
    //ra和a共享一块空间谁改了另一个就改了

    注意:引用类型必须和引用实体是同种类型的

    5.3引用特性

  • 引用在定义时必须初始化
    不然无法知道我引用的是那块空间

  • 一个变量可以有多个引用
    一个共享单车可以很多人骑吗,一块空间可以由多个引用来管理。

  • 引用一旦引用一个实体,再不能引用其他实体
    一个共享单车只能一个人骑,我管理这块空间了就不能再去管理别的空间了。

  • void TestRef()
    {
    int a = 10;
    // int& ra; // 该条语句编译时会出错(引用必须初始化)
    int& ra = a;
    int& rra = a;
    printf("%p %p %p\\n", &a, &ra, &rra);
    }

    5.4使用场景

  • 做参数
  • void Swap(int& left, int& right)
    {
    int temp = left;
    left = right;
    right = temp;
    }

  • 做返回值
  • int& Add(int a, int b)
    {
    static int c = a + b;
    return c;
    }
    int main()
    {
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
    }

    做返回值的时候我们要注意一下如果我们不加static的话他出了add这个函数的栈帧他会把空间给OS,此时这个空间就不安全了你外面引用就可能出现问题。此时如果你调用一个函数压栈返回回来的c就是随机值了。

    在这里插入图片描述
    总结
    如果出了函数作用域还在 用引用可以少开空间提高效率。

    5.5 权限

    先看一段代码

    void TestConstRef()
    {
    const int a = 10;
    //int& ra = a; // 该语句编译时会出错,a为常量
    const int& cra = a;
    int b=100;
    const int& crb=b;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    }

    这里我们发现ra,rd都会报错我们一个一个来分析

    aconst修饰只读
    ra 可读可改
    cra 只读
    b 可读可改
    crb 只读

    只有在引用处指针才会有权限的概念。
    a:就像我们都是管理一块空间我是老大我对这块空间只读,你引用了我还可以改了不行,是权限的放大。
    b:就像我们都是管理一块空间我是老大我对这块空间可读可改,你引用了只读可以,是权限的缩小。
    d:类型决定了我们对一块空间的访问方式,我对这块空间一次访问8个字节,你只访问4个字节不行

    5.5传值、传引用效率比较

    以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

    写个代码验证一下

    #include <ctime>
    struct A
    {
    int a[10000];
    };
    void TestFunc1(A a)
    {
    }
    void TestFunc2(A& a)
    {
    }
    int main()
    {
    A a;
    // 以值作为函数参数
    size_t begin1 = clock();
    for (size_t i = 0; i < 10000; ++i)
    TestFunc1(a);
    size_t end1 = clock();
    // 以引用作为函数参数
    size_t begin2 = clock();
    for (size_t i = 0; i < 10000; ++i)
    TestFunc2(a);
    size_t end2 = clock();
    // 分别计算两个函数运行结束后的时间
    cout << "TestFunc1(A)-time:" << end1 begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 begin2 << endl;
    return 0;
    }

    在这里插入图片描述
    通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

    5.6 引用和指针的区别

  • 引用概念上定义一个变量的别名,指针存储一个变量地址。
  • 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
  • 引用在对象前面(例:int* b=&a)还是取地址
    引用在类型后面(例:int& rb=b)变成了引用
  • 引用在定义时必须初始化,指针没有要求(尽量初始化)
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  • 没有NULL引用,但有NULL指针
  • 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  • 有多级指针,但是没有多级引用
  • 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  • 引用比指针使用起来相对更安全
    引用不需要assert断言。指针很容易出现空指针和野指针的问题
  • 六、内联函数

    6.1 概念

    以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。内联函数的出现,直接取代了C语言中的宏函数。
    正常的函数是到调用的位置会通过地址去找这个函数的定义,我们来看汇编下的函数调用,call表示函数跳转地址
    在这里插入图片描述
    如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数调用。在这里插入图片描述
    这里我们发现他没有函数调用了

    6.2 特性

  • inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
    2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。 但是合理使用可以将内联函数的缺点缩小到极小(可忽略不计)。编译器会帮助你合理使用。
    3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
  • 6.3与宏的区别

    // 实现一个ADD宏函数的常见问题
    //#define ADD(int a, int b) return a + b;
    //#define ADD(a, b) a + b;
    //#define ADD(a, b) (a + b)

    // 正确的宏实现
    #define ADD(a, b) ((a) + (b))

  • 为什么宏函数不能加分号?

    宏函数在使用时会进行宏替换,将宏名部分全部替换为宏体,如果宏体有分号,会被一块儿替换进去,产生编译错误

  • 为什么要有外括号?.为什么要有内括号?

    优先级很错乱,错误的千奇百怪,尽量在每一个变量加一个括号(防御性编程)

  • 我们可以得出以下结论

  • 不方便调试宏。(因为预编译阶段进行了替换)

  • 导致代码可读性差,可维护性差,容易误用。

  • 没有类型安全的检查 。
    以上的缺点我们都可以使用inline来克服。

    七、auto关键字(C++11)

  • 7.1引入

    后面我们会学到迭代器类型有这种的

    std::map<std::string, std::string>::iterator

    这里我们要打特别多字所以auto应运而生

    7.2概念

    使用auto定义变量时必须对其进行初始化(不然无法推导),在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto推导出变量实际的类型。

    int TestAuto()
    {
    return 10;
    }

    int main()
    {
    int a = 10;
    auto b = a;
    auto c = 'a';
    auto d = TestAuto();
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;
    }

    7.3 auto的使用细则

  • auto与指针和引用结合起来使用
    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&
  • int main()
    {
    int x = 10;
    auto a = &x;
    auto* b = &x;
    auto& c = x;
    cout << typeid(a).name() << endl;
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    *a = 20;
    *b = 30;
    c = 40;
    return 0;
    }

  • 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
  • void TestAuto()
    {
    auto a = 1, b = 2; //int a=1,b=2
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
    }

    7.4 auto不能推导的场景

  • auto不能作为函数的参数
  • // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
    void TestAuto(auto a)
    {}

  • auto不能直接用来声明数组
  • void TestAuto()
    {
    int a[] = {1,2,3};
    auto b[] = {456};
    }

    在这里插入图片描述

    7.5总结

    auto在实际中最常见的优势用法就是跟下面会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

    八、 基于范围的for循环(C++11)

    8.1引入

    在C和C++11之前我们要写for都要像怎么写

    int main()
    {
    int array[] = { 1, 2, 3, 4, 5 };
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
    array[i] *= 2;
    for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
    cout << *p << endl;
    }

    我们发现写这种有范围的for非常麻烦因此就引入了基于范围的for循环(C++11)

    8.2语法

    for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量这里我们就回收上面讲的auto了,第二部分则表示被迭代的范围。

    int main()
    {
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
    e *= 2;
    for(auto e : array)
    cout << e << " ";
    return 0;
    }

    注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

    8.3 范围for的使用条件

  • for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法(后面会讲到的迭代器),begin 和 end 这两个方法定义了 for 循环迭代的起始和结束范围。
    注意:以下代码就有问题,因为for的范围不确定。
  • void TestFor(int array[])
    {
    for(auto& e : array)
    cout<< e <<endl;//错误
    }

  • 迭代的对象要实现++和==的操作。(后面具体讲)
  • 九、指针空值nullptr(C++11)

    9.1 引入

    在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

    void TestPtr()
    {
    int a=0;
    int* p1 = NULL;
    int* p2 = &a;
    // ……
    }

    NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码

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

    可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。

    9.2NULL的问题

    先看以下的这个代码

    void f(int)
    {
    cout<<"f(int)"<<endl;
    }
    void f(int*)
    {
    cout<<"f(int*)"<<endl;
    }
    int main()
    {
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
    }

    在这里插入图片描述

    我们本来是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,所以打印了f(int)
    在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量, NULL是(void*) 因为函数参数是 int 所以发生了隐式类型转换,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

    9.3解决

    因此我们在C++11引入了nullptr他就是一个空指针了(void*)0。
    在这里插入图片描述

    9.4总结

  • 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  • 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  • 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
  • 赞(0)
    未经允许不得转载:171主机测评 » 【C++】 入门篇 从C到C++
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址