C++入门


前言

本文适合学过循环分支语句、函数、数组、结构体、指针等知识,或者是有C语言基础的同学想要进一步学习C++的同学阅读。如果循环分支、函数等等这些还不懂,不建议阅读。


一、前言

1.什么是C++

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。

1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计


2.C++的发展史

1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。

语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看一下C++的历史版本。

阶段内容
C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等
C++1.0添加虚函数概念,函数和运算符重载,引用、常量等
C++2.0更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数
C++3.0进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理
C++98C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
C++03C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性
C++05C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即:计划在本世纪第一个10年的某个时间发布
C++11增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等
C++14对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等
C++17在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等
C++20自C++11以来最大的发行版,引入了许多新的特性,比如:**模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)**等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等
C++23制定ing

C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所有大家不用追求最新,重点将C++98和C++11掌握好,等工作后,随着对C++理解不断加深,有时间可以去琢磨下更新的特性。


3.C++关键字

C++总计63个关键字。
ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲。
在这里插入图片描述
大家也别觉得多,其实这里面包括了C语言中的关键字
在这里插入图片描述


二、C++入门知识

1.命名空间

(1)为什么会有命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
简单来说就是解决命名重复这个问题的
例如:

#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main() {
    printf("%d\n", rand);
    return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”

在这里插入图片描述


(2)命名空间的定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
1. 正常的命名空间定义

// 1. 正常的命名空间定义
namespace a //namespace + 名字(名字你可以随便取,一般开发中是用项目名字做命名空间名。)
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;
    int Add(int left, int right) 
    {
        return left + right;
    }
    struct Node 
    {
        struct Node* next;
        int val;
    };
}

2.嵌套定义
就是命名空间里面可以再套一个命名空间:

namespace M
{
    int a = 10;
    int b = 20;
    namespace N
    {
        int c = 30;
        int d = 40;
    }
}

为了说明解决命名冲突这个问题,可以举个非常简答的例子:你看图中我们直接定义了两个全局变量rand,按以前的来说这肯定会报重定义的错误的,但我们用了命名空间后就解决了这个问题,虽然我rand名字一样,但我一个是命名空间a里的rand,一个是命名空间b里的rand那肯定不一样啦。
在这里插入图片描述

#include <iostream>
using namespace std;

namespace a
{
    int rand = 10;
}
namespace b
{
    int rand = 20;
}
int main() 
{
    cout << "命名空间a中的rand = " << a::rand << endl;
    cout << "命名空间b中的rand = " << b::rand << endl;
    return 0;
}


(3)命名空间的使用

1. 使用命名空间名称+ :: (作用域限定符)
符号"::"在C++中叫做作用域限定符,我们可以通过命名空间名称 :: 命名空间成员的方式访问到命名空间中的内容。

#include <iostream>
using namespace std;

namespace M
{
    int a = 10;
    int b = 20;
}

int main() 
{
    cout << M::a << endl << M::b << endl;
    return 0;
}

2. 使用using将命名空间中的成员引入
例如:可以发现输出a就不需要M::a这么写了,那是因为前面使用了using M::a。但是b还得写M::b,如果b也不想这样写了,那就只能前面再加一条using M::b。
在这里插入图片描述
3. 使用using namespace 命名空间名称引入
这样做就是将这个命名空间中的内热全部引入了,凡是在这个命名空间中的内容都不需要通过命名空间名称+::+变量名称的方式访问了。
在这里插入图片描述


2.C++中的输入和输出

新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,
在这里插入图片描述
那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。

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

在C语言中我们是使用printf和scanf来实现输出和输入的,而在C++中我们是使用cin和cout来实现输入输出的,下面是对这段程序的详细解释:

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

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即 可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不 带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用<iostream>+std 的方式。
由于C++中的输入输出是不需要增加数据格式控制的,会自动识别的。不像C语言那样%d啊、%s啊等等。由此观之C++的输入输出更加方便了。
在这里插入图片描述

#include <iostream>
using namespace std;

int main() 
{
    int a=10;
    float b=3.14;
    char str[7] = "string";
    cout << a << endl << b << endl;
    cout << str;
    return 0;
}
// ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因
//为C++兼容C语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,我们再配合文
//档学习。

std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?

  1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
  2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。

3.缺省参数

(1)缺省参数的概念

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

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

(2)缺省参数的分类

全缺省参数
全缺省参数即函数的所有形参都给一个默认值,即全部都设置为缺省参数

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

半缺省参数
半缺省参数,即函数的参数不全为缺省参数,但这里有个四个规定:
1. 半缺省参数必须从右往左依次给出,不能隔着给。

void Print(int a, int b = 20, int c) //错误示例
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

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

//错误示例
//test.h中声明
void Print(int a = 10, int b = 20, int c = 30)//test.cpp中定义
void Print(int a = 10, int b = 20, int c = 30) 
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

缺省参数只能在函数声明中出现,或者函数定义时出现,二者只能其一,不可以同时出现。

3. 缺省值必须是常量或者全局变量。
4. C语言不支持(编译器不支持)。


4.函数重载

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

(1)函数重载的概念

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

#include <iostream>
using namespace std;
// 1、参数类型不同
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;
}
// 2、参数个数不同
void f() 
{
    cout << "f()" << endl;
}
void f(int a) 
{
    cout << "f(int a)" << endl; 
}
// 3、参数类型顺序不同
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;
}
int main() 
{
    Add(10, 20);
    Add(10.1, 20.2);
    f();
    f(10);
    f(10, 'a');
    f('a', 10);
    return 0;
}

(2)extern “C”

由于C和C++编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如:

  1. C++中调用C语言实现的静态库或者动态库,反之亦然
  2. 多人协同开发时,有些人擅长用C语言,有些人擅长用C++

在这种混合模式下开发,由于C和C++编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景下,就需要使用extern “C”。在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。
注意:在函数前加extern "C"后,该函数就不支持重载了。


5.引用

详情见我的另一篇文章引用详解,在这篇文章里我对引用进行了详细的阐述。


6.内联函数

(1)内联函数的概念

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

#include <iostream>
using namespace std;

int Add(int a, int b)
{
    return a + b;
}
int main() 
{
    int ret = Add(10, 20);
    return 0;
}

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
内联函数:

#include <iostream>
using namespace std;

inline int Add(int a, int b)
{
    return a + b;
}
int main() 
{
    int ret = Add(10, 20);
    return 0;
}

非内联函数的汇编:
在这里插入图片描述
内联函数的汇编:
没有函数调用
在这里插入图片描述


(2)内联函数的特性

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

7.auto关键字(C++11)

(1)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;
}

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


(2)auto的使用规则

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

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

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

(3)auto不能推导的场景

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

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

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

void TestAuto()
{
	int a[] = {1,2,3};
	auto b[] = {456};
}

3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。


8.基于范围for的循环(C++11)

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

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

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


(2)范围for的使用条件

1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定

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

2. 迭代的对象要实现++和= =的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现
在大家了解一下就可以了)


9.空指针nullptr

(1)C++98中的空指针

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

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

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

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

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。


(2)C++11中的空指针

对于C++98中的问题,C++11中引入了关键字nullptr。
注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

本篇文章就到这里就结束了,希望对各位有所帮助,喜欢的友友们别忘记三联哦!我们下篇再见,拜拜!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

争不过朝夕,又念着往昔

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

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

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

打赏作者

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

抵扣说明:

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

余额充值