C++入门

时间:2024-02-21 22:49:03

本篇是作为C++学习的第一篇博客,主要讲解一些入门知识,为后续学习打基础。

C++是在C的基础上,容纳了面向对象编程思想,并增加了许多有用的库以及编程范式等。因此C++是完全兼容C的,C代码不经过任何修改即可在C++编译器下编译运行。

本篇博客的主要目标是:补充C语言语法的不足,掌握C++是如何对C语言设计不合理的地方进行优化的。

一、C++关键字

C语言有32个关键字,C++98有63个关键字(见下图),C++11有73个关键字。

这里不对其详解,需要用到时再细讲。

二、命名空间 

命名空间的出现是为了解决命名冲突的问题。在C语言中,全局域内不允许出现完全相同的标识符,而在大型工程中难免会有同名现象,这带来了很大的不便,因此C++出现了命名空间,将相同的标识符放在不同的命名空间里就可避免冲突。

1.定义

命名空间的定义需要使用关键字namespace,后面跟命名空间的名字,再接一对{ }即可。命名空间内可以定义变量、函数、结构体。

注意:命名空间的定义没有‘;’,要与结构体的定义区别开来。

namespace name
{
    int rand = 10;//变量
    
    int Add(int left, int right)//函数
    {
        return left + right;
    }

    struct Node//结构体
    {
        struct Node* next;
        int val;
    };
}

命名空间支持嵌套:

namespace N1
{
    int a;
    int b;
    int Add(int left, int right)
    {
        return left + right;
    }

    namespace N2
    {
        int c;
        int d;
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}

同一项目中允许存在多个相同名称的命名空间,编译器会自动将它们合并成同一个命名空间。

2.使用

一个命名空间相当于定义了一个新的作用域,若想使用里面的标识符,必须指明命名空间。

方法有三:

①在标识符前加命名空间名称和作用域限定符(: :

int main()
{
    printf("%d\n", name::rand);//::为作用域限定符
    return 0;    
}

②使用using将命名空间里的成员引入

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

③使用"using namespace  命名空间名称"将命名空间展开

using namespce name;
int main()
{
    printf("%d\n", rand);
    Add(10, 20);
    return 0;    
}

3.std

std是C++标准库的命名空间,库里的各种接口函数均在该命名空间里。

在写代码时经常会用到库里的标识符,为了方便,在平常练习中,可以展开该命名空间,这样可以直接使用而不用加作用域限定符。但在大型工程中一般不建议这样使用,因为大型工程标识符众多,可能会与标准库的标识符冲突。

usinng namespace std;

三、C++输入、输出

C++也有一套自己的输入输出方案:

#include<iostream>
using namespace std;
int main()
{
    int a=0;
    cin>>a;//输入
    cout<<"Hello world!!!"<<endl;//输出
    return 0;
}

说明:

①使用cincout时,必须包含头文件#include<iostream>。上面代码中,将std展开了,因此可以直接使用;如果不展开,需要加作用域限定符std::cinstd::cout

②在这里,>>是流提取运算符,<<是流插入运算符

endl是特殊的C++符号,表示换行,与\n作用相同,也包含在iostream头文件里

④使用C++的cin/cout比C语言的scanf/printf更方便,不需要控制格式,它们可以自动识别变量类型

⑤(该点对于初学者可能不懂,学习了类与对象就明白了)cincout实际上分别是istreamostream类的对象。在C语言里<<>>分别是左移位和右移位操作符,这里涉及到了运算符重载的知识。

四、缺省参数

1.概念

缺省参数是在声明或定义函数时,为函数参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

void Func(int a = 0)
{
    cout<<a<<endl;
}

int main()
{
    Func();     // 没有传参时,使用参数的默认值0
    Func(10);   // 传参时,使用指定的实参10
    return 0;
}

2.分类

①全缺省参数

void Func(int a = 10, int b = 20, int c = 30)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}

②半缺省参数

void Func(int a, int b = 10, int c = 20)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}

3.注意

①半缺省参数必须从右往左依次给出,不能间隔。(即所有半缺省参数必须放在普通参数的后面)

②缺省参数不能在声明和定义时同时出现。一般推荐出现在声明中。

③缺省值必须是常量或全局变量

④C语言不支持缺省参数

五、函数重载

1.概念

函数重载是函数的一种特殊情况,C++允许在同一作用域内声明多个功能相近的同名函数,这些函数的形参列表不同(类型不同、数量不同、类型数量相同但顺序不同),常用来处理功能类似而数据类型不同的问题。

注意:函数重载只对函数名和形参列表有要求,与返回值无关。因此,当两个函数的函数名和形参相同而返回值不同时,是不构成重载的。

2.原理——名字修饰

为什么C++支持函数重载而C语言不支持呢?

原因是C++编译器会对函数名进行修饰,比如g++会将函数名修饰为“_Z+函数长度+函数名+类型首字母”,这样构成重载的函数经过修饰后其函数名就会变得不一样,在链接阶段就可以根据调用情况而选择对应的函数。但C语言编译器不会对函数名进行修饰,导致同名函数在链接时无法区分,进而报错。

六、引用

1.概念

int main()
{
    int a = 10;
    int& ra = a;//定义引用变量ra
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

引用是为已经存在的变量取一个别名,这个别名也被叫做引用变量。引用变量的定义需要使用&。例如上述代码中,引用变量ra就是a的引用。

编译器不会对引用变量开辟内存空间,它和被引用的变量共用同一块内存空间。因此,上述代码的打印结果是一样的。

注意:引用变量和引用实体必须是同类型的。

2.引用的特性

①引用变量在定义时必须初始化

②一个变量可以有多个引用

③引用变量一旦定义就不可改变引用实体

3.常引用

常引用就是在引用时加const修饰符。常引用表示引用变量本身是只读的,即无法通过引用变量修改变量的值。

因此,常引用变量可以对const修饰的变量或常量进行引用。此外,常引用变量还可以对普通变量(没有被const修饰的变量)进行引用。但是,普通引用变量是不能对const修饰的变量或常量进行引用的(原因:const修饰的变量或常量本来是只读的,但普通引用变量可读可写,这样通过引用变量这个别名就可以修改变量的值,显然是不合理的。)。为了方便记忆,可以理解为:权限只能平移和缩小,但不能扩大

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;     // 错误,权限扩大
    const int& ra = a; // 正确,权限平移
    
    // int& b = 10;    // 错误,权限扩大
    const int& b = 10; // 正确,权限平移
    
    double d = 12.34;
    double& rd1 = d;    // 正确,权限平移
    const double& rd2 = d; // 正确,权限缩小
}

4.使用场景

①做形参

void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

例如上述代码中的交换函数,在C语言中要想实现两个数的交换必须传指针,而C++里形参可以使用引用,实参不需要指针,直接传递变量即可。可见,引用的使用让传参更加方便。

②做返回值

int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

引用做返回值时,函数会返回变量的引用(也即别名,至于具体的名字是什么,不用关心,这是编译器的工作)。因此,使用引用返回时,返回的变量必须是全局变量或静态局部变量,否则在函数栈帧销毁后局部变量不复存在,返回其引用会带来未知错误。

补充说明:当函数使用值返回时,函数会返回变量的一份临时拷贝,虽然这时可以返回局部变量,但是效率是非常低下的,尤其是当返回值类型很大时,效率会更低。因此,优先使用引用返回,效率更高。

5.引用VS指针

语法概念上,引用只是一个别名,没有独立空间,和其引用实体共用一块内存空间;而指针通常是指指针变量,用来存放地址,独自占用空间,大小为4或8个字节。

底层实现上,引用其实是占用空间的,因为引用是按照指针的方式来实现的

引用和指针的不同点:

①引用在定义时必须初始化;指针没有要求

②引用在初始化时引用一个实体后,就不能再引用其他实体;而指针变量可以随意修改指向

③没有空引用;但有空指针

④有多级指针,但没有多级引用

⑤引用比指针使用起来相对更安全

七、内联函数

1.概念

inline修饰的函数叫做内联函数。编译阶段编译器会在调用内联函数的地方将其展开,用函数体替换函数的调用,没有函数调用建立栈帧的开销,可以提升程序的运行效率。

2.特性

inline是一种以空间换时间的做法,虽然减少了调用开销,提高了效率,但是可能导致代码膨胀,使目标文件变大

②内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求

inline修饰的函数其声明和定义必须在一起,不能分离(这里的分离指的是声明和定义在不同的源文件里)。为什么呢?首先要知道,inline函数在编译阶段就会展开,因此只有在其定义的源文件中可以成功展开,而且inline函数是不会进入符号表的。当声明和定义分离时,声明放在头文件中,定义放在源文件1中,这时另一个源文件2包含该头文件想要使用该内联函数时,当头文件展开后,只有声明,没有定义,编译阶段无法展开,依旧保持函数调用的格式。到了链接阶段,由于符号表中没有该内联函数,无法成功链接,报错——“无法解析的外部符号”。(如果还是理解困难,可以参考博客【程序环境和预处理】中的编译过程)

④C++规定:在类定义中定义的成员函数都是内联函数(即使不加inline也是)

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

1.新的含义

随着程序越来越复杂,用到的类型也越来越复杂,有的类型冗长难于拼写,若多次使用还需拼写多次,非常不方便。比如后面将会学到的map迭代器的类型:

std::map<std::string, std::string>::iterator it = m.begin();

如何解决上述问题呢?一种方法是使用typedef进行类型重定义:

typedef std::map<std::string, std::string> Map_iterator;

这种方法虽然可行,但终究还是要写一次类型,能否做到一次都不用写呢?因此C++11给auto赋予了新的含义。

在早期的C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但是由于它可以省略,一直没有人使用它。

于是,在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量的类型必须由编译器在编译期间自动推导而得。

需要注意的是,使用auto定义变量时必须对其进行初始化,这样编译器才有了推导依据——根据初始化表达式来推导变量的类型。编译器在编译期间会将auto替换为变量的实际类型。

2.使用细则

①用auto声明指针类型时,用autoauto *没有任何区别;但是用auto声明引用类型时必须加&

②使用auto在同一行声明多个变量时,这些变量必须是相同类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

void TestAuto()
{
    auto a = 1, b = 2; 
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

③auto不能作为形参类型,因为编译器无法对其实际类型进行推导,即使有缺省值也不行

④auto不能用来声明数组

⑤为了避免与C++98中的auto发生混淆,C++11中只保留了auto作为类型指示符的用法

⑥auto在实际中最常见的优势用法是跟C++11中新增的新式for循环和lambda表达式进行配合使用

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

1.语法

C++98中要想遍历一个数组,可以使用这样的代码:

void TestFor()
{
    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;
}

这样的写法虽然不难,但写的多了就有点麻烦。而且,对于有范围的集合而言,由程序员来说明遍历范围是多余的。因此,C++11中引入了基于范围的for循环:for循环后的括号由‘’分为两个部分,第一部分是用于迭代的变量,第二部分则表示用来迭代的范围。

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

注意:与普通循环类似,可以用continue来结束本次循环,用break来退出循环

2.使用条件

①for循环迭代的范围必须是确定的:

对于数组而言,就是第一个元素和最后一个元素之间;对于容器而言,必须提供begin和end方法,begin和end就是循环迭代的范围。

例如,下面的代码就是有问题的,虽然形参表面上是数组,但实际是一个整型指针,从而导致for的范围不确定:

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}

②迭代的对象要实现++和==操作

十、空指针nullptr(C++11)

既然有了NULL,为什么C++11还要引入nullptr呢?究其原因还是比较复杂的,限于篇幅,将在后续博客【】进行解释,这里暂时记住即可:在支持C++11的编译器下写代码时用到空指针最好使用nullptr

注意:使用nullptr时不需要包含头文件,因为nullptr在C++11中是作为新的关键字引入的