C++入门

简介: 正如标题一样,顾名思义,本节内容主要目的就是快速入门C++这门语言,很多人就想问了,为什么C++这么难的语言可以快速入门呢?不会又是标题党吧,嘿你别说,还真不是,因为在看本节内容之前我是默认你是扎扎实实的学过一遍C语言的,只有有了一门编程语言的基础之后才可以快速入门其他一门语言,当然这门语言最好是C语言,C生万物不是说说而已,强烈不推荐没有学习C语言的直接学C++。

zai@TOC

C++入门

正如标题一样,顾名思义,本节内容主要目的就是快速入门C++这门语言,很多人就想问了,为什么C++这么难的语言可以快速入门呢?不会又是标题党吧,嘿你别说,还真不是,因为在看本节内容之前我是默认你是扎扎实实的学过一遍C语言的,只有有了一门编程语言的基础之后才可以快速入门其他一门语言,当然这门语言最好是C语言,C生万物不是说说而已,强烈不推荐没有学习C语言的直接学C++。

好了回来继续说我们的C++,C++是在C的基础上,容纳进去了面向对象的编程思想,并增加了许多有用的库,以及编程范式等,学习C当然也是对C++有一定帮助的,本节C++入门这节具体的目标:

  1. 补充C语言语法的不足,以及了解C++是如何对C语言设计不合理的地方进行优化的,例如:作用域方面,IO方面,函数方面,指针方面,宏方面等。
  2. 为后续类和对象打基础。

好了,说了这么多,那么开始本节内容的学习吧。

C++的关键字(C++98)

C++中63个关键字,C语言32个。

asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast

我们这里只列举一下,简单看一下不做具体解释,其实里面有很多C语言中学过的,但是也有很多可能看不懂的,没有关系,想一下当初学习C语言时一上来不也是看32个关键字一脸懵吗?之后慢慢都会理解的。

命名空间

命名空间

在C/C++中,变量、函数、以及后面要学习的类,都是大量存在的,这些函数变量类的名称如果都存在于全局作用域中,就会造成很多冲突。那么使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突和污染,那么命名空间的关键字叫做namespace,名字起的也是非常容易理解。

看下面一段程序,理解一下什么是命名冲突:

#include <stdio.h>
#include <stdlib.h>
int rand = 0;
int main()
{
    printf("%d", rand);
    return 0;
}

image-20230413095950249

直接编译的话可以发现编译器直接就报错了,rand重定义,便是我们所说的命名冲突,在stdlib这个头文件中已经有了一个名叫rand的函数,我们这里又定义了一个rand变量,编译当然也就出错了。

命名空间定义

那么我们如何用命名空间来解决上面的冲突问题呢?其实很简单,我们只需要将我们自己定义的变量装到一个命名空间域里面即可。

namespace lzb
{
    int rand = 0;
}
int main()
{
    printf("%d", lzb::rand);
    return 0;
}

这里我们自己定义的命名空间域,名字就叫做lzb,下去练习建议用自己的名字缩写即可,而一般开发中是用项目名字做命名空间名

下面在使用我们自己的命名空间域内的东西时,使用了一个::作用域限定符,就是用来指定使用哪个作用域中的变量函数等等。

  • 一个命名空间就构成了一个新的作用域。

上面就是最基本的命名空间定义和使用的方式。

下面我们具体来看一下几种命名空间的特点:

1.正常的命名空间定义(常用)

namespace lzb
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;
    int Add(int left, int right)
    {
        return left + right;
    }
    struct Node
    {
        struct Node* next;
        int val;
    };
}

2.命名空间可以嵌套(常用)

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;
        }
    }
}

3.命名空间是开放的,即可以随时把新的成员加入已有的命名空间中(常用)

也就是说编译器最后会将多个命名空间内的东西合并。

namespace A {
   
   
    int a = 100;
    int b = 200;
}
//将c添加到已有的命名空间A中
namespace A {
   
   
    int c = 300;
}

4.命名空间中的函数 可以在“命名空间”外定义

namespace A {
   
   
    int a=100;//变量
    void func();
}
void A::func()//成员函数 在外部定义的时候 记得加作用域
{
   
   
    //访问命名空间的数据不用加作用域
    cout<<"func遍历a = "<<a<<endl;
}
void funb()//普通函数
{
   
   
    cout<<"funb遍历a = "<<A::a<<endl;
}
void test06(){
   
   
   A::func();
    funb();
}

5.无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接(了解)

namespace{
   
   
    int a = 10;
    void func(){
   
   
        cout<<"hello namespace"<<endl;
    }
}
void test(){
   
   

    //只能在当前源文件直接访问a 或 func
    cout<<"a = "<<a<<endl;
    func();
}

6.给命名空间起个别名(了解)

namespace veryLongName{
   
   
    int a = 10;
    void func(){
   
    
        cout << "hello namespace" << endl; 
    }
}
void test(){
   
   
    namespace shortName = veryLongName;
    cout << "veryLongName::a : " << shortName::a << endl;
    veryLongName::func();
    shortName::func();
}

上面代码里涉及到一个coutendl,先只知道cout是C++中的输出,endl是换行即可。

以上几种情况其实不必全部掌握,有些只做了解即可,将常用的嵌套、添加成员等最常用的理解即可。

命名空间使用

命名空间定义我们学会了,那么怎么使用呢?C++中提供了三种使用方式。

来看这样一个命名空间,看如何访问其成员,

namespace lzb
{
    // 命名空间中可以定义变量/函数/类型
    int a = 0;
    int b = 1;
    int Add(int left, int right)
    {
        return left + right; 
    }
    namespace lzb2
    {
        int c = 100;
        struct Node
        {
            struct Node* next;
            int val;
        };
    }
}
  • 加命名空间名称及作用域限定符
int main()
{
    cout << lzb::a << lzb::b << endl;//访问a和b
    cout << lzb::lzb2::c << endl;//访问c
    return 0;
}
  • 使用using将命名空间中某个成员引入
using lzb::a;
using lzb::lzb2::c;
int main()
{
    cout << a << c << endl;
    return 0;
}
  • 使用using namespace 命名空间名称引入(命名空间展开)
using namespace lzb;
using namespace lzb::lzb2;
int main()
{
    cout << a << b << endl;
    cout << c << endl;
    return 0;
}

C++输入&输出

新生的婴儿会以自己独特的方式来向这个崭新的世界打招呼,C++刚出来之后,也是一个新的事物,那C++怎么来向这个美好的世界来生问候呢?

我们来看学习C++的第一段程序,

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

和学习C语言时我们写的第一句代码一样,hello world!!!有没有勾起你一些初次接触编程时的回忆呢,所有程序员的第一段代码,也是有那么一点点浪漫的嘛。

言归正传,我们来看其中的输出语句,

说明

1. 使用 cout标准输出对象(控制台)和 cin标准输入对象(键盘),必须包含头文件,以及按命名空间规则使用 std

2. coutcin是全局的流对象, endl是C++特殊的符号,表示换行,都包含在中。
3. <<是流插入运算符, >>是流提取运算符。
4. 使用C++输入输出更方便,不需要像 printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
5. 实际上 coutcin分别是 ostreamistream类型的对象, >><<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。

注意:

> 早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式。

下面可以自己写一下简单的输入输出代码熟悉一下即可,感受一下C++的输入输出比C语言方便在了哪里,

c++ #include <iostream> using namespace std; int main() { int a; double b; char c; // 可以自动识别变量的类型 cin >> a; cin >> b >> c; cout << a << endl; cout << b << " " << c << endl; return 0; }

关于 coutcin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,实际上这些又用得不是很多,平时如果需要控制格式的话其实用 printf是很方便的。

image-20230416154004432

可以看一下这些格式,如果你想这样写的话,也可以记住。这里还有一篇推荐的博客,如果有需要可以点进去看: http://t.csdn.cn/r6ntO

# 缺省参数

## 缺省参数概念

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

c++ void Func(int a = 0) { cout << a << endl; } int main() { Func();//没有传参时,使用参数的默认值 Func(10);//传参时,使用指定实参 return 0; }

## 缺省参数分类

- 全缺省参数

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

- 半缺省参数

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

注意:
  1. 半缺省参数只能从右向左依次给出,不能间隔

  2. 缺省参数在函数的定义和声明中不能同时出现

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

  3. 缺省值必须为常量或者全局变量(一般为常量)

通常在声明处给缺省值。

函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,我国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”

函数重载的概念

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数类型类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

C++中其实就是解决函数名不能重名的问题,带来很大的不方便,例如两个整数相加和两个浮点数相加,如果非得定义成两个函数就有些过于麻烦了,像这种功能类似的函数当然还是同名很方便。

  • 参数类型不同
int Add(int left, int right)
{
    cout << "int Add(int left, int right)" << endl;
    return left + right;
}

double Add(double left, double right)
{
    cout << "double Add(double left, double right)" << endl;
    return left + right;
}
  • 参数个数不同
void f()
{
    cout << "f()" << endl;
}

void f(int a)
{
    cout << "f(int a)" << endl;
}
  • 参数类型顺序不同
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;
}

思考:为什么不设置返回值不同呢?

想一下在调用函数的时候,我们是不写返回值的,那编译器怎么能知道你要调用哪个函数呢。

C++函数重载的原理

上面我们只是了解了一下什么是函数重载,也知道了怎么用,但是还不够,因为我们不知道它是通过什么实现的,那么下面我们就来看一下函数重载的实现方法,以及为什么C语言不支持,C++是怎么进行优化的。

  • C++函数重载底层原理是基于编译器的 name mangling 机制。

以下面两个函数为例,我们在linux环境下可以验证:

我们知道编译链接分为几个阶段,每个阶段做什么事情来回顾一下:

  1. 预处理阶段:删注释,头文件展开,宏替换,条件编译...

  2. 编译:语法检查,符号汇总,生成汇编代码...

  3. 汇编:形成符号表,生成二进制机器代码...

  4. 链接:合并段表,符号表的重定位和合并...

我们知道在链接时,要通过符号表来寻找函数等一些标识符的地址,那么符号表中的函数名是怎么定义的呢,其实C++的函数重载就是通过重新设计这个名称来实现的,举个例子:

我们先写出一段C语言代码来看一下C语言中的函数名修饰规则,我们是在Linux中来展示,

其中所看到的信息是用下面一句指令来看到的:

objdump -S 目标文件

image-20230416170922763

这是C语言中的函数修饰规则,直接用函数名,而下面我们用同样的方法,看一下C++的修饰规则是什么样的,

image-20230416171136585

可以看到,已经不是简单的函数名了,下面来解释一下这个命名规则,我们就以_Z3Fundd这个为例吧:

image-20230416171703785

其他函数名修饰规则也是同理,通过这种方法,在符号表中的函数名就有区别,在链接时也就可以根据你的函数参数来区别你要调用的是哪个函数,这就是C++函数重载的作用原理。

值得一提的是,不同的环境下的名字修饰规则并不相同,我们上面是用的linux下g++编译器来展示的,而在windows系统vs编译器中的命名规则要复杂的多,例如下面的示例:

image-20230416172022130

所以我们一般都是用Linux下来演示,两者思想都是一样的,只是在实现上有差别。

引用

引用概念

引用不是新定义一个变量,而是给已经存在的空间取一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同一块空间。

比如一个形象的例子:李逵,在家叫铁牛,江湖人称黑旋风。虽然有多个名字,但是本质都是这个人。

引用方式:

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

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

int main()
{
    int a = 10;
    int& ra = a;
    cout << a << endl;
    cout << ra << endl;
}

类型不一致则会报错:

image-20230420143903655

引用特性

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

image-20230420144303111

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

image-20230420144623465

  • 引用在初始化时引用一个实体后,就不能再引用其他实体

常引用

image-20230420145300268

如果想对常量进行引用,必须将引用变量也加上const修饰,

void TestConstRef()
{
    const int a = 10;
    //int& ra = a; // 该语句编译时会出错,a为常量
    const int& ra = a;

    //int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
}

对于常引用还有一种情况,

void TestConstRef()
{
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
    cout << rd;
}

image-20230420150904309

这时候有些类似于强制转换,加上常属性则可以引用不同类型的变量。

使用场景

1.做参数

最经典的例子就是两数交换,若按照之前的方法写就是用指针,而现在可以使用引用这一更方便的方式:

void Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

2.做返回值

首先要清楚一点,我们的函数为什么要使用引用来返回,

  1. 函数需要返回的值比较小的时候,返回时通过寄存器来暂时保存,但是寄存器只有4/8个字节,并且数量也很有限,
  2. 函数需要返回的值比较大的时候,直接返回其实是会形成临时变量的,

函数需要返回的值如果特别大的话,那么每次都要形成很大临时变量,造成的开销一定就很大。这时候返回指针当然是一种方式,但是我们今天学习了引用,那么引用可不可以呢?

看下面一段代码:

image-20230420152710423

你会发现非常奇怪的一幕,为什么三次的ret打印出来都不一样呢,并且只有第一次的ret是正确的。

这就又要提及到之前的函数栈帧了,函数用引用做返回的是c的别名吗?我们看这段代码似乎是的,但实际上并不是这么简单,在编译器编译时,这段代码是会报出一个警告的,

image-20230420153603170

所以实际上我们返回的是c的地址回来,而由于c是在函数Add中定义的,函数Add调用完就会销毁,将此段栈帧还给操作系统,下面再去访问实际上已经是非法访问了,同时我们也发现了,函数销毁并不是将那段空间数据清空,而仅仅是将使用权还给操作系统,所以第一次访问时访问到了正确的数据,但是下面又调用了其他函数,只要又有操作用了那段空间,其中的值就会改变,所以在后面两次访问到的值都发生了变化。

所以通过上面的分析就可以知道,如果需要返回一个较大的数据时,正确使用引用返回可以极大的避免空间的消耗,但是好看的玫瑰却是带刺的,用此方法实现起来缺尤为需要注意非法访问的问题。

总结:

如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

传值和传引用效率的比较

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

上面我们已经分析了传值和传引用效率的问题,那么两者有多大差别呢。

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

image-20230420155414447

当传引用作为函数参数,和传值相比,两者效率看起来差了不是一星半点。

  • 作为函数返回值
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 1000000; ++i)
        TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 1000000; ++i)
        TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

image-20230420155837919

看起来传引用的效率还是非常高的,但是还是要提醒,传引用作返回值,慎用!

引用和指针的区别

引用和指针其实这两个玩意关系很微妙,

语法层面上讲,引用就是一个别名,没有独立空间,和其引用体共用同一块空间。这是我们对引用的理解。

但是从底层上来看,引用实际上是有空间的,因为引用的底层就是用指针来实现的

这个其实我们也可以观察到,

image-20230420161054295

可以看到,汇编代码是一模一样的。

引用和指针的不同点

  1. 引用从概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体
  4. 没有NULL引用,但有空指针
  5. sizeof中含义不同:引用结果为引用类型大小,但指针始终是地址空间所占字节个数(32位平台下4个字节)
  6. 引用自加即引用的实体加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但没有多级引用
  8. 访问实体方式不同,指针需要解引用,引用编译器自己处理
  9. 引用比指针使用起来更加安全

内联函数

概念

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

通常情况下,对于一些简短并且使用频繁的函数我们才使用内联函数不要把所有函数都设为内联函数,具体原因后面会提到。

内联函数是否展开我们怎么能知道呢?如何去进行验证,我们先来看一个函数

inline int Add(int a, int b)
{
    return a + b;
}
int main()
{
    Add(1, 2);
    return 0;
}

这个函数很简短了吧,我们也将它定义成了内联函数,但是编译器有没有按照我说的去做呢,我们可以来验证一下,还是通过汇编代码的方式来看,正常情况下调用函数一定有一条call指令,如果我们没有看到call指令说明函数展开了,而不是去调用这个函数。

image-20230420163112889

坏了,这编译器怎么回事,为什么没有按照我说的去做呢?

实际上编译器当前的版本是debug版本,VS在debug版本下默认内联函数是生效的,因为debug版本本来就是调试版本,不会对代码做出优化。

下面可以在realease版本下看一下,

image-20230420163556826

可以看到确实没有call指令,说明确实没有去调用函数,而是直接展开了。

但是另外还要补充一下,debug版本下的内联函数是可以通过项目属性设置更改的,下图是VS2022下的演示,

image-20230420164143413

特性

  1. inline是一种以空间换时间的做法,如果编译器将函数作为内联函数处理,在编译阶段会将函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了函数调用,提高程序运行效率。

  2. inline对于编译器只是一个建议,不同编译器对于内联函数的实现机制并不完全相同,一般建议:将函数规模较小(其实就是函数体比较短,不同编译器实现不同,没有确定说法),不是递归,且频繁调用的函数使用inline修饰,否则编译器会忽略你的建议。

    image-20230420170242796

  1. inline不建议定义和声明分离,分离会导致链接错误。因为inline被展开后,是没有函数地址的,最后链接通过符号表去找是找不到的。建议将内联函数之间写到头文件中。

    image-20230420170709950

对于这种短小使用频繁的函数为了减少消耗,在C++内联函数出现之前的解决方式是用宏函数,但是用宏去实现,又会有很多问题,

宏的优缺点:

优点:

  1. 增强代码复用性
  2. 提高性能

缺点:

  1. 不方便调试(宏是在预处理编译阶段进行替换)
  2. 导致代码可读性差,可维护性差,容易误用
  3. 没有类型安全的检查

C++有哪些可以代替宏?

  1. 常量定义:const enum

  2. 短小函数定义,换用内联函数

auto关键字

为什么要有auto

要想先简单提一下auto的作用,根据右边的参数自动推导类型,例如:

auto a =10;
int a=10;

上诉两种写法完全等价,这样看起来似乎auto也没有什么价值,我还按原来那么写就可以了嘛。

那是因为我们现在所使用的类型还很简单,到后面我们会有一些名字特别长的情况,例如:

#include <string>
#include <map>
int main()
{
    std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
    "橙子" },
    {"pear","梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    while (it != m.end())
    {
        //....
    }
    return 0;
}

std::map<std::string, std::string>::iterator这是一个类型,但是这类型太长了,很容易出错。可能有的同学会想到用typedef来进行一下重命名,这确实是一种解决方式,但是还不够,这样在有些情况下又会引起新的问题:

typedef char* pstring;
int main()
{
    const pstring p1; // 编译成功还是失败?
    const pstring* p2; // 编译成功还是失败?
    return 0;
}

在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11中,标准委员会赋予了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;
    //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
    return 0;
}

typeid这个操作可以打印出变量的类型。

注意:

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

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;
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

image-20230420175146916

auto不能推导的场景

  1. auto不能作为函数的参数

image-20230420175225903

  1. auto不能用来直接声明数组

image-20230420175338563

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

基于范围的for循环

范围for的语法

在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循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

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

两种写法都可以很好的遍历数组,但是两种写法有什么区别呢,auto&是引用可以修改数组中的值,而只用值是改不了的。

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

范围for的使用条件

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

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

image-20230420215006383

图中的函数参数是一个指针,而不是数组,此时范围就是不确定的。

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

这个迭代器的概念以后再看。

指针空值nullptr

C++98中的指针空值

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

void TestPtr()
{
    int* p1 = NULL;
    int* p2 = 0;
    // ……
}
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

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;
}

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void )0。

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
40 2
C++入门12——详解多态1
|
2月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
28 3
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
30 2
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
81 1
|
2月前
|
程序员 C语言 C++
C++入门5——C/C++动态内存管理(new与delete)
C++入门5——C/C++动态内存管理(new与delete)
71 1
|
2月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
20 1
|
2月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
35 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
47 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
23 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
27 0