C++入门全集(1):初窥门径

一、前言

C++是一种计算机高级程序设计语言,它在C语言的基础上进行了进一步的扩充和完善,并增加了许多有用的库,是一种面向对象的程序设计语言。

所以,C++是兼容C语言语法的。

我打算把所有C++入门需要学习的知识整合成一个全集,方便各位,也能方便自己复习。

本文主要讲解C++相对C语言的查漏补缺和优化部分,也为后续学习类和对象打基础。

二、C++关键字

C语言有32个关键字,而C++有63个关键字,不过这些关键字无法一次学完,我们这里先混个脸熟

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

三、命名空间

在C语言中,命名冲突问题时常存在,例如我们无法定义一个名为rand的变量,因为在stdlib.h中已经有函数取名为rand了

如果我们将所有的变量名、函数名和类名都存放在全局作用域中,就可能导致命名冲突,所以C++中出现了命名空间这一概念。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。

3.1 命名空间的定义

要定义一个命名空间,需要使用到关键字namespace,然后给命名空间取个名,再用大括号括起来,大括号中即为命名空间的成员

namespace test
{
    int rand = 10;
}

除了变量,命名空间中还可以定义函数或者结构体类型

另外,命名空间还可以嵌套,如:

namespace test1
{
    namespace test2
    {
        int rand = 10;
    }
}

如果命名空间也同名了怎么办呢?同一个工程的不同文件中可能出现多个同名的命名空间,此时编译器会将它们合并为同一个命名空间。

3.2 命名空间的使用

当我们学会在命名空间中定义变量、函数和类型后,该如何使用它们呢?

(1)使用作用域限定符

所谓作用域限定符就是两个冒号 "::"

像这样,就可以访问到两个不同命名空间的变量a了

(2)使用using引入命名空间成员

变量b是命名空间test1的成员,我们可以通过using引入它,所以下面main函数中我们访问变量b就不需要使用作用域限定符了

(3)使用using namespace

像这样,我们使用using namespace引入命名空间test1,所以没有使用作用域限定符的a就访问到了test1中的a,而下面使用了作用域限定符就会访问test2中的a

在后面学习C++的输入和输出中,我们会学到cout和cin,二者的定义是放在名为std的一个C++标准库的命名空间中的,C++将标准库的定义实现都放在这个命名空间中。

如果不使用using namespace,每次使用的时候就要用到作用域限定符,也就是变成std::cout和std::cin,十分的麻烦。

所以我们在使用它们的时候一般会先输入“using namespace std;”,避免每次要输入和输出的时候都使用作用域限定符。

四、C++的输入和输出

听说每一个程序员的第一个程序都是hello world,我们来使用C++实现一下

#include <iostream>

using namespace std;

int main()
{
    cout << "Hello world" << endl;
    return 0;
}

从这一段简短的代码中,我们可以总结如下信息:

  • cout和cin是全局的流对象,endl是特殊的C++符号,表示换行,它们都包含在<iostream>头文件中
  • <<是流插入运算符,>>是流提取运算符

实际上,cout和cin分别是ostream和istream类型的对象,而<<和>>在C语言中原本是位运算符,在C++却变了,这里也涉及到运算符重载的知识,在后续我们还会对其进行深入的学习

注意:早期的标准库将所有功能都在全局域中实现,并声明在以.h为后缀的头文件中,使用时也只需要包含对应头文件即可。而后来则将其实现在std命名空间下,为了和头文件区分,规定C++的头文件不带.h。一些旧编译器可能还支持<iostream.h>的格式,不过还是推荐使用<iostream>+std的方式

使用cout和cin的好处在于更方便,不需要像printf和scanf一样手动控制格式

缺点在于打印一串数据的情况会比较繁杂

所以我们根据实际情况选择更优的方式

在日常练习中,我们当然可以使用using namespace std,怎么方便怎么来。

但是这样做,整个标准库就全部暴露出来了,此时如果我们定义了和库中重名的类型/对象/函数,就会出现冲突问题。所以在项目开发中不推荐使用全局展开,我们可以指定命名空间访问,例如using std::cout和using std::cin来展开常用的命名空间成员

五、缺省参数

缺省参数就是我们声明或定义函数时可以为函数的参数指定一个值,在调用函数的时候没有指定的实参,就使用这个预先设定的默认值

缺省参数又分为全缺省参数和半缺省参数

关于缺省参数,我们需要注意几点:

(1)半缺省参数必须从右到左依次给出,不能间隔着给

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

像这样,函数的定义和声明中同时出现缺省参数,而两个位置设置的值不同,编译器就无法确定该使用哪个缺省值

(3)缺省值必须是常量或者是全局变量

六、函数重载

C++允许在同一作用域中声明几个功能类似的同名函数,前提是这些同名函数的形参个数/类型/类型顺序不同。

例如我们以前在C语言中想实现Add函数,但是int和double类型的数据不能用同一个函数处理,每处理一种类型的数据就要写一个函数,函数间还不能同名

但是在C++中针对这个问题进行了优化,编译器会有一套自己的函数名修饰规则来修饰不同形参个数/类型/类型顺序的同名函数

#include <iostream>
using namespace std;

//参数类型不同
int Add(int x, int y)
{
    return x + y;
}

double Add(double x, double y)
{
    return x + y;
}

//参数个数不同
void f(int a)
{
    cout << "void f(int a)" << endl;
}

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

//参数类型的顺序不同
void f(int a, char b)
{
    cout << "void f(int a, char b)" << endl;
}

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

int main()
{
    Add(1, 2);
    Add(1.1, 2.2);

    f(1);
    f(1, 2);

    f(10, 'a');
    f('a', 10);

    return 0;
}

C++支持函数重载的原理就是——名字修饰(name Mangling)

名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制。简单来说就是编译器为了区分多个同名函数,规定了一个新的规则来对原本的名字进行修饰

为什么C语言不支持函数重载,是因为它的名字修饰规则过于简单,只是在函数名前面添加了下划线

拓展:在C++的函数前加上extern "C" 就可以让函数按照C语言的风格编译

这里可以看到,Add函数按照C语言的风格编译后名字变为了_Add

而在C++中,修饰规则得到了完善,所以可以支持函数重载

不过不同的编译器有自己的函数名修饰规则,上面的就是在Windows下vs的修饰规则,有点过于复杂了,有兴趣的同学可以自行深入学习。

接下来我们展示g++的修饰规则,它会比前者更加的简单易懂

#include <iostream>
using namespace std;

int Add(int x, int y)
{
    return x + y;
}

double Add(double x,double y)
{
    return x + y;
}

int main()
{
    Add(1, 2);
    Add(1.1, 2.2);
    return 0;
}

上面的是源文件代码,我们在终端中输入"g++ -S test.cpp -o test.s"来查看其汇编代码

可以看到,这两个就是上面int类型的Add函数和double类型的Add函数重载后的名字了。

其中_Z后跟着的数字就是原函数名的长度,后面的 "ii" 和 "dd" 就是参数的类型。

通过这些,我们就能理解为什么C语言不支持函数重载,而C++通过函数名的修饰规则可以区分同名函数了,只要参数个数/类型/类型顺序不同,修饰后的名字就不同,也就可以区分了。

需要注意的是,如果两个函数的函数名和参数个数/类型/类型顺序都相同,也是无法区分的。

七、引用

引用就是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间,而是和它引用的对象共用同一块内存空间,引用的符号和取地址符号&一样。

需要注意的是,引用变量必须和被引用的对象是同一类型的

引用变量也可以作为被引用的对象

引用还可以代替指针的传址调用,例如以前我们要实现Swap函数,需要指针传地址才能成功交换,现在可以换成引用,此时形参是实参的别名。

当然,指针也可以进行引用

7.1 引用的特性

(1)引用在定义时必须初始化

(2)一个变量可以有多个引用,也就是取多个别名

(3)一旦引用变量已经引用过了某个对象,就再不能引用其他对象

可以看出,引用和指针还是有区别的,像在链表这些地方我们还是选择使用指针,而在一些输出型参数,也就是形参的改变要影响实参的地方我们可以选择使用引用

7.2 常引用

(1)如果被引用的对象被const修饰,而引用变量没有被const修饰会报错

(2)引用变量如果没有被const修饰,不能将常量作为引用对象

(3)引用变量没有被const修饰且和被引用对象不是同一类型时会报错

7.3 引用的使用场景

(1)引用作为函数参数,就是上面提到过的Swap

(2)引用作为函数返回值

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);

	cout << "Add(1, 2) is :" << ret << endl;
	cout << "Add(1, 2) is :" << ret << endl;

	return 0;
}

上面的代码会输出什么结果?

实际上,结果是未定义的。

第一次调用Add函数的时候,函数栈帧创建完毕,局部变量c(此时值为3)保存在Add的栈帧中。函数运行结束后,栈帧销毁,内存空间被系统回收,此时变量c已经没有意义了,所以ret引用了一块已经被释放的空间。

第二次调用Add函数和第一次一样,只不过局部变量c的值变为了7。

需要注意的是,虽然空间被回收,里面的东西却都还在。就像去住酒店,退房后里面的东西在没打扫前都是一直保持原样的,而不是说退房后里面的东西就都没了。

所以,当我们第一次输出ret的值是7,第二次就变为了随机值

因此,如果出了函数作用域后返回对象没有销毁(static,malloc等),则可以使用引用返回,否则必须使用传值返回

另外的,引用返回还可以修改返回的对象

#include <iostream>

using namespace std;

int& Func(int* a, int i)
{
	return a[i];
}

int main()
{
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for(int i = 0 ;i<10;i++)
	{
		Func(a, i) = i * 10;
		cout << a[i] << " ";
	}
	return 0;
}

7.4 传值和引用返回的效率比较

以值作为函数参数或返回值类型的时候,函数并不会直接传递实参或者将变量本身直接返回,而是会创建一个临时变量作为“中间商”,因此会影响效率,我们以传值返回为例测试一下

#include <iostream>
#include <time.h>
using namespace std;

struct A 
{ 
	int a[10000];
};

A a;

// 值返回
A TestFunc1()
{
	return a;
}
// 引用返回
A& TestFunc2()
{ 
	return a;
}

int main()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();

	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();

	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
	return 0;
}

可以看到,传值返回比引用返回效率低了许多

7.5 引用和指针的区别

从底层来看,引用还是按照指针的方式来实现的,我们可以观察一下汇编代码

二者的不同点在于:

  • 从概念上来说,引用是定义了一个变量的别名,指针是存储了变量的地址
  • 引用在定义时必须初始化,指针可以不用
  • 引用在初始化时引用了一个对象后,就不能再引用其他对象,而指针可以随时改变指向
  • 没有空引用,但是有空指针
  • 从sizeof来说:引用的sizeof结果是引用类型的大小,但指针始终是地址所占字节个数
  • 引用的++是被引用的对象+1,而指针++是指针向后偏移一个类型的大小
  • 多级指针之间的含义不同,但多级引用指向的是同一块空间
  • 访问对象的方式不同,指针需要解引用,而引用是由编译器自己处理
  • 引用比指针使用起来更安全

八、内联函数

在C++中,为了解决一些频繁调用的小函数大量消耗栈内存的问题,引入了inline修饰符

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开函数,不用调用函数建立栈帧。 

其作用很像宏函数,不过相比宏函数的缺点,它可以调试,有类型的检查,不容易出错。

inline int Add(int x, int y)
{
	return x + y;
}

inline是一种比宏更简单更好的方式,并且不降低效率。

如果不用inline修饰,在汇编代码中我们会看到用call指令去调用函数的操作

但是在Add函数前增加inline将其修改为内联函数,在编译期间编译器会用函数体替换函数的调用。

我们可以在release模式下查看汇编代码中是否存在call Add的指令

如果想在debug模式下查看,需要进行一些设置,因为在debug模式中默认情况下编译器不会对代码进行优化

在项目中点击属性

在属性中修改这两项

然后我们在debug模式下查看内联函数的汇编代码

可以看到,已经没有call指令了。

inline是一种以空间换时间的做法,这里的空间指编译出来的可执行文件的大小,内联函数会直接在程序中展开,越长的函数调用越多次就会使文件大小暴增。

inline只是向编译器发出的一个请求,编译器可以选择忽略该请求

假如有一个1000行的函数,我们要在程序中调用1000次,如果不用inline修饰的话就是1000+1000行代码,如果用inline修饰的话就是1000*1000行代码,换做是你你觉得哪个更好?

可以看到,太长的函数尽管用了inline修饰,编译器也会忽略掉,选择call调用函数

一般建议将函数规模较小、非递归且调用次数较多的函数使用inline修饰

需要注意的是,inline不建议声明和定义分离,会导致链接错误,对于内联函数最好直接在头文件中定义

此时在test.cpp中,编译时展开了Func.h的内容,而里面只有Func函数的声明,只能等链接的时候在符号表中寻找对应函数,也就是通过call指令调用函数,而内联函数是不会生成call指令的

所以我们直接在头文件中定义内联函数即可

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

随着我们不断深入学习C++,程序越来越复杂,使用的类型也会越来越复杂,不仅难于拼写,还容易出错。

除了typedef,我们还有另一个选择就是auto,它可以帮助我们自动推导类型

需要注意的是,使用auto定义变量时必须对变量进行初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。

因此,auto并非是一种类型的声明,而是一个占位符,编译器在编译时会将auto替换为变量实际的类型 

9.1 使用auto的注意事项

(1)如果auto后加上*就限定了赋值的对象必须是指针

(2)我们使用auto时,也可以在同一行定义多个变量,前提是这些变量必须是相同的类型

(3)用auto声明引用类型时还是要加上&的

#include <iostream>
using namespace std;

int main()
{
	int a = 10;
	auto& b = a;

	return 0;
}

9.2 不能使用auto的场景

(1)auto不能在函数的参数中使用

(2)auto不能用于声明数组

十、范围for(C++11)

现代C++倾向于让各种繁杂的操作变得简洁,因此诞生了许多语法糖,范围for算是其中的典型。

在C++98/03中,不同的容器和数组遍历的方式有很多,不够统一,也不够简洁。

而C++11出现了基于范围的for循环,可以更简洁的去遍历容器和数组,也更方便我们使用了。

以前我们遍历数组的方式如下:

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

对于一个有范围的集合而言,由程序员来声明循环的范围未免太多余,还容易出错。接下来我们来使用范围for遍历数组:

for循环的括号中由冒号":"分为两部分,左边是范围内用于迭代的变量,右边表示被迭代的范围

这里也用到了前面的auto关键字,如果我们想对范围内的元素进行修改,还可以用到引用&

和普通循环一样,范围for中也可以使用continue和break。

需要注意的是,范围for迭代的范围必须是确定的。对于数组而言,就是数组第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

void Func(int array[])
{
	for (auto x : array)
		cout << x << endl;
}

像这种情况就不能使用范围for,因为传参到函数中时传递的不是一整个数组而是数组指针,此时for的范围不确定。

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

在过去,我们给一个没有指向的指针进行初始化的时候会使用NULL,而NULL实际上是一个宏。

我们在C语言中使用NULL没有问题,但是在C++中就会出现问题,为什么呢?

在传统的C头文件stddef.h中,可以看到如下代码:

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

可以看到,在C++中NULL被定义为0,这样会造成什么麻烦呢?

可以看到,就算传递的参数为NULL,程序还是会调用int类型的Func,而不是int*的Func,这违背了我们的目的。

因此出现了指针空值nullptr来填补这个bug,使用nullptr时不需要包含头文件,因为它是C++11作为新关键字引入的。为了提高代码的健壮性,我们后续表示指针空值时最好都使用nullptr。

有人会想,为什么不直接把这个bug修改了呢?因为语言有一个向前兼容的原则,也就是已经出现的东西即使有问题也不能修改,如果贸然去修改了可能会导致以前的代码无法运行,可能会造成巨大的损失。

到这里,我们就算对C++有了一个简单的了解了,下一篇的C++入门我们就会开始学习类和对象。

如果觉得本文对你有帮助就点个赞吧

完.

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值