C++入门必备知识

1. 命名空间

要知道,在大多数情况下实际开发过程中参与者不止一位,而变量名和函数名是要有具体含义的,所以在一个工程中不可避免地会出现名字相同的情况.为了解决C遗留下来的问题,C++给出了解决方案:命名空间(关键字:namespace)

1.1 定义命名空间

格式如下:

namespace [命名空间名]
{
//空间体:
//变量
//函数
}

例子:``

namespace name1
{
    int a = 10;
    int b;
    void func()
    {
        printf("nemespace\n");
    }
}

命名空间的用法就是用括号将可能出现命名冲突的变量或函数括起来,然后给它起个名字.这个命名空间的作用域就是大括号内.
对于未初始化的变量,其默认值为0.

1.2 命名空间的使用

对于作用域name1来说,使用该空间中变量或函数的方法有三种.

1.2.1 作用域限定符

作用域限定符即::,需要紧跟命名空间使用.
例子:

int main()
{
	name1::func();
	printf("%d\n", name1::a);
	return 0;
}

1.2.2 使用using引入命名空间

使用using关键字引入命名空间,让程序去指定命名空间找到变量或函数.
例子:

using namespace name1;
int main()
{
	func();
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}

1.2.3 使用using和作用域限定符

使用using和作用域限定符引入命名空间中特定的变量或函数.
例子:

using name1::a;
using name1::func;
int main()
{
	func();
	printf("%d\n", a);
	return 0;
}

1.3 命名空间的注意事项

  • 日常使用(非工程中)关键字using时,常常会写出using namespace std;这样的语句,它的意思是将标准函数库(这是一个命名空间,里面有很多东西)的所有变量展出.将它们展出有时会发生冲突,所以这种写法仅适用于日常练习(为了方便,不用一个一个地展出),实际项目中不建议这么做.
  • 命名空间是可以嵌套的.
  • 如果在外部调用的变量或函数在命名空间和main函数中的名字都相同,会出现"编译错误".原因是编译器处理变量的流程:首先从全局域开始,如果没有,会在局部域查找,否则编译错误.从这句话我们可以得出结论:访问命名空间的变量,必须通过空间名搭配::using访问.

2. 输入和输出

在学习C++的输入和输出操作之前,需要知道终端是什么:简单地由键盘和屏幕组成,称为控制台.
C++和C语言一样,它本身并未定义如何输入输出的语句,换言之,输入输出语句都是人们后来写的,标准委员会(管这个语言的机构)为了统一输入输出的使用,使用了一个提供包含IO机制(input & output)的标准库(standard library).这个标准库中有很多功能,但对于C++的学习,我们只需要了解IO库的基本概念和操作即可.
iostream库是常用的IO库之一,它包含istream(输入流)和ostream(输出流).一个字符序列就是一个"流",是从终端读取或写入终端的.“流”(stream)的概念表示字符是随着时间退役
C++是一门面向对象的语言(不是完全面向对象),所以各种数据对它而言就是一个个对象.输入输出的东西也是如此.(在学了类和对象后才能对此处的知识有较深的理解)

2.1 输入输出对象

标准库定义了4个IO对象:cin,cout,cerr,clog.主要了解前两个.

  • cin:读作:see-in,被称为标准输出,是istream类型对象.
  • cout:读作:see-out,被称为标准输出,是ostream类型对象.

输出&&输入运算符:

  • 输出运算符(流插入):<<,它的左侧运算对象必须是ostream对象,右侧运算对象是要打印的值.其返回值是左侧对象的值.
  • 输出运算符(流提取):>>,与输出运算符类似.

操作符:

  • endl,简单理解为换行.

例子:

#include <iostream>
int main()
{
	int a = 1, b = 2;
	std::cout << a << std::endl;
	std::cout << b << std::endl;
	return 0;
}

结果:

1
2

注意事项:

  • 使用标准库中的对象,需要包含头文件,一些老旧编译器支持<iostream.h>的写法,但这是被淘汰的.
  • endl操作符是可以添加多个的,就像换行符一样.
  • 为了练习中使用方便,可以写成这样
#include <iostream>
using namespace std;
int main()
{
	int a = 1, b = 2;
	cout << a << endl;
	cout << b << endl;
	return 0;
}

2.2 优点

C++的输出操作是不需要像C一样指定格式输入或输出的,这提高了效率.
假若需要指定格式输出,要知道C++是兼容C的,所以可以使用printf语句.

3. 缺省参数

在C语言中,调用函数传参必须指定每一个参数的值,这样强制有一个缺点,也许函数中的某个参数无需修改或未知,其默认值也未知,C++给出了解决办法.
缺省参数,也叫预设参数,默认实参.即函数定义时将不必须指定值的参数给予初始值(默认值),然后传参时任由函数调用者给该参数传参或不传参.传参则用传入的参数,否则使用默认的初始值.

3.1 缺省参数的使用

例子1:

//全缺省
#include <iostream>
using namespace std;
void func(int a = 1, int b = 2, int c = 3)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
	func();
	return 0;
}

结果:

1
2
3

所有参数都是不必指定值的参数,称为全缺省.所有参数在函数定义时都赋予初始值,不传参则所有变量的值都为初始值.
如果传入参数为(4,5,6),打印结果也为4 5 6
例子2:

//半缺省
#include <iostream>
using namespace std;
void func(int a, int b = 2, int c = 3)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
	func(1);
	return 0;
}

结果:

1
2
3

与全缺省相对地,只有部分参数是缺省参数,称为半缺省.
缺省参数在定义时的顺序必须是从右往左连续缺省.传参时的顺序从左往右.

//正确的定义[以它为例]
void func(int a, int b = 2, int c = 3)
//错误的定义
void func(int a = 1, int b, int c)//必须是从右往左
void func(int a = 1, int b, int c = 3)//必须是连续的
//正确的传参
void func(4)//忽略bc
void func(4,5)//忽略c
void func(4,5,6)//全修改
//错误的传参
void func(,5,6)//a不是缺省参数
void func(4,,6)
//...

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

//test.h
#pragma once
void func(int a, int b = 2, int c = 3);
//test.cpp
#include <iostream>
#include "test.h"
using namespace std;
void func(int a, int b = 2, int c = 3)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
	func(4, 5, 6);
	return 0;
}

错误:重定义默认参数.
原因是编译器无法在定义和声明中给出的缺省值做出选择,即使定义中的缺省参数的值与声明相同.这样做的目的是避免声明和定义时给出的缺省参数不相同的情况.

3.2 应用场景

缺省参数通常应用于某个参数很多次都是同一个值,或者初始值就是某个值,比如在初始化某个容器的容量.那么第一次使用它时就可以不用传参,使用默认值.

4. 函数重载

在同一作用域中,几个函数的函数名相同但形参列表不同,则将其称之为重载函数(overload).

形参列表:参数的类型,数量和顺序
注意:函数之间构成重载,与返回值无关.

4.1 定义和使用重载函数

下面以多个打印函数为例:

#include <iostream>
using namespace std;
void Print(int a, int b)
{
	cout << a << " " << b << endl;
}
void Print(int a, double b)
{
	cout << a << " " << b << endl;
}
void Print(double a, int b)
{
	cout << a << " " << b << endl;
}
void Print(double a, double b)
{
	cout << a << " " << b << endl;
}
int main()
{
	Print(1, 2);
	Print(1, 2.2);
	Print(1.1, 2);
	Print(1.1, 2.2);
	return 0;
}

结果:

1 2
1 2.2
1.1 2
1.1 2.2

显而易见,函数重载弥补了C语言的缺点,减轻了程序员记忆函数名字的负担,只需要通过一个名字,搭配不同参数,即可实现对多种数据组合的处理.
实际上,C++中的输入输出语句支持任意类型任意搭配的组合,也是通过"重载"实现的.

4.2 名字修饰

C++支持函数重载,是因为它将参数列表也作为函数身份的一部分,说明C++和C在底层对函数名称的处理有着区别.
要知道,C/C++从源文件到可执行程序主要需要四个步骤:预处理,编译,汇编,链接.
在编译以后,各个.cpp文件中的函数名(处理过的)将会对应一个地址,存放在符号表中,而链接的主要步骤就是通过符号表,找到函数真正的地址,然后通过函数之间的调用关系将它们链接到一起.
要找到函数的地址,需要通过函数名称查找.而C和C++在编译阶段对函数名称的处理的方式是不同的.简而言之,C对函数名称几乎不做处理,而C++将函数的形参列表作为函数名称的一部分.这就是C不支持函数重载而C++支持的原因.正因如此,函数的返回值与函数重载无关.

4.3 extern “C”

接着上面的话题,C++支持函数重载而C不支持,假如一个C++程序需要使用由C实现的某些函数呢?
如果不加任何处理地在C++程序中使用C实现的功能,会出现链接时错误,也就是说,这样做会造成链接失败.
原因同上,C++编译时会将参数列表作为函数名称的一部分,而C不会.使用C++编译器编译C程序,最后会让C函数的名称发生错误,造成链接错误.
解决办法是在C实现的函数前加上语句 extern "C".这样做的目的是告诉C++编译器按照以C的方式编译该部分代码.换句话说,让C++的编译器不要将该部分的函数参数列表作为函数名称的一部分.

#include <iostream>
using namespace std;
extern "C" void func()
{
	cout << "extern" << endl;
}
int main()
{
	func();
	return 0;
}

关于编译链接

5. 引用

引用(reference),是为对象起了另外一个名字,以"&变量名"来定义引用类型.
也就是说,引用是变量的别名

#include <iostream>
using namespace std;
int main()
{
	int a = 20;
	int& ra = a;//定义引用类型

	cout << a << endl;
	cout << ra << endl;
	//打印地址
	cout << &a << endl;
	cout << &ra << endl;

	return 0;
}

结果:

20
20
0335FAB0
0335FAB0

定义了引用后,该引用和原来的对象是绑定在一起的,它们都指向同一块空间.

5.1 特性

  • 引用类型必须与引用对象的类型相同
  • 引用在定义时必须初始化
  • 一个对象可以有多个引用
  • 一个引用只能引用第一个对象,且可以更新这个对象的值.也就是说,一个引用一旦被定义了,它指向的空间不会再发生变化,后续的赋值操作只是改变它引用的对象的值,并不会改变它引用的对象.
int main()
{
	int a = 20;
	int& ra = a;

	//double& ra = a;//引用类型不匹配
	int rra = a;//一个对象可以有多个引用
	int b = 10;
	ra = b;
	return 0;
}

5.2 使用场景

5.2.1 做参数

  1. 输出型参数

例如交换函数:

void swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 1, b = 2;
	swap(a, b);
	return 0;
}

如果使用指针,稍微麻烦一点,首先需要传变量的地址,然后在函数内部对指针解引用.用引用传参代替一级指针,比较方便且直观.

  1. 大对象传参

使用引用给大对象传参是相对于传值传参而言的.假设有一个很大的结构体,里面有很大 的数组,如果传值传参,需要临时拷贝一个相同的数组,而引用传参是不需要这样做的,它就相当于传数组的指针,开销很小.在语法上,引用是不需要额外空间的.

struct A
{
	int a[100000];
	int data;
};
void func(A& a)
{
	//statement
}
int main()
{
	A a;
	
	func(a);
	return 0;
}

对于大对象来说,传参的方式决定了效率.
请注意,main函数和func函数中的a是不同的变量.因为它们不在同一个作用域.

5.2.2 做返回值

首先来看传值返回:

#include <iostream>
using namespace std;
int func()
{
	int count = 0;
	count++;
	return count;
}
int main()
{
	int ret = func();
	cout << ret << endl;
	return 0;
}

显而易见,结果是1.
至关重要的是,传值返回返回的不是该函数栈帧的count,而是它的拷贝.原因是main函数调用func函数完毕后,返回值还未返回func的栈帧就已经被销毁了.事实上返回值如果不是很大的话是通过寄存器保存的,如果返回值很大,系统会在main函数中开辟一个空间存放返回值.

有人会说:原因是count是局部变量,如果用static修饰它,系统就不会返回它的拷贝.按道理来说是这样的,毕竟这样能省下拷贝的开销.然而事实上编译器并没有那么智能,只要是传值返回,一律返回拷贝后的值,不论返回值是局部变量还是全局变量.

再来看传引用返回:

#include <iostream>
using namespace std;
int& func()
{
	int count = 0;
	count++;
	return count;
}
int main()
{
	int& ret = func();
	cout << ret << endl;
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}

结果:

1
2038602120
2038602120

结果显示,第一次的结果是正确的.事实上,第一次的结果只是歪打正着(大多数情况也是如此).首先用函数栈帧理解这段代码.函数返回的是conut的别名(这个别名系统自己起,假设为tmp).第一次返回tmp,指向的空间已经被销毁了,只不过它存放的值还未被覆盖.一旦使用了它的值,就会被其他函数覆盖,成为随机值.
出现随机值的原因:越界.而避免越界的准则就是严格限定边界,也就是定义域.一旦出了函数的作用域,就只能使用传值返回.
只有这种情况适合用传引用返回:

#include <iostream>
using namespace std;
int& func()
{
    //将count的定义域扩大
	static int count = 0;
	count++;
	return count;
}
int main()
{
	int& ret = func();
	cout << ret << endl;
	return 0;
}

结果:

1

5.3 const的引用

const修饰,表示对象被读写的权限缩小.被const修饰的变量,其值不能够被修改,是读写权限被缩小为读.
把引用的对象绑定到被const修饰的对象上,就像绑定在其他对象上一样.唯一的区别在于const的引用不能够修改.

#include <iostream>
using namespace std;
int main()
{
	//权限平移
	int a = 10;
	int& b = a;
	//查看a和b的类型
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	//权限不能放大
	const int c = 20;
	const int& d = c;
	//权限可以缩小
	int e = 30;
	const int& f = e;

	cout << b << endl;
	cout << d << endl;
	cout << f << endl;
	return 0;
}

结果:

int
int
10
20
30

权限平移:a变量有被读或写的权限,而b引用也有被读或写的权限.

权限不能放大:c变量只有被读的权限,那么d也只能被读.
权限缩小:e变量有被读或写的权限,f变量有被读的权限.

5. 引用和指针的关系

从语法上看,引用是对象的别名,和对象共用同一块空间.从内存角度看并非如此.

5.1 共同点

从汇编理解引用和指针的共同点:

  int a = 10;
  mov         dword ptr [a],0Ah  
//引用的汇编代码
    int& ra = a;
  lea         eax,[a]  
  mov         dword ptr [ra],eax  
    ra = 20;
  mov         eax,dword ptr [ra]  
  mov         dword ptr [eax],14h        
//指针的汇编代码
    int* pa = &a;
  lea         eax,[a]  
  mov         dword ptr [pa],eax  
    *pa = 20;
  mov         eax,dword ptr [pa]  
  mov         dword ptr [eax],14h  

通过汇编代码可以发现,在底层中引用和指针的指令完全相同,原因是引用实际上是指针实现的,可以认为引用就是封装后的指针.所以指针能用的地方都可用引用替代.
所以从底层上理解,引用是占有空间的,效率和指针相当.

5.2 区别

指针和引用的区别:

  • 引用必须初始化
  • 只能引用一次,没有多级引用.因为引用本身不是对象,是对象的别名
  • 引用指向的对象不能修改,指针指向的对象可以任意修改
  • sizeof(引用)的值是引用对象的值;sizeof(指针)的值是指针所占空间大小
  • 引用自增1表示引用的对象自增1,而指针自增1表示指针指向的位置向后跨一步(步长取决于类型)
  • 引用访问对象编译器自动处理,指针访问对象需要程序员手动解引用访问

小结:

引用就是封装后的指针,相较于指针,它使用起来更安全,更直观,相对局限.

6. 内联函数

在函数的返回值类型前用inline修饰,将它声明为内联函数(inline).它的作用是向编译器发出一个请求,编译时C++编译器会在调用内联函数的地方展开函数,当然在某些情况下编译器也可以忽略这个请求.
例如:

#include <iostream>
using namespace std;
inline int min(int a, int b)
{
	return a < b ? a : b;
}
int main()
{
	cout << min(2, 1) << endl;
	return 0;
}

展开后的结果:

cout << {
	a < b ? a : b;
}
<< endl;

6.1 特点

  • 内联函数inline只是向编译器发出请求,执行与否取决于代码本身.假如代码过长(75行以上),递归函数都不太可能展开,因为这样做的代价比函数压栈的代价更大.换句话说,内联函数适合短小的函数以及被频繁使用的函数,展开的收益大于函数压栈.例如堆排和快排中的交换函数.
  • 内联是一种空间换时间的做法.
  • 内联尽量不要分离声明和定义.否则很有可能会出现链接错误.原因是内联被展开后函数地址随着函数名消失,无法链接.报错:无法解析的外部命令.

6.2 与宏的关系

在C中,宏可以说是一大利器,它减少栈帧的开销,提高了效率,复用性强,但有着难以解决的缺点,例如宏不方便调试,没有类型安全检查,可读性差(宏函数).
而C++青出于蓝而胜于蓝,用inline弥补了宏的所有缺点.使用内联函数在符合条件的地方展开.
《effectIve C++》一书中提到:尽量使用const、enum和inline替代宏。
总的来说,inline兼具宏的优点,又弥补了宏带来的缺点。

“不符合条件”:函数内部代码指令长度较长(10行),有递归等,编译器会优化,从而忽略内联.如果展开会导致编译出来的.exe程序很大.原因是当调用次数很大且指令长度很大时,不展开的效率更高.

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

使用类型说明附auto,它能让编译器替我们分析表达式所属类型.auto定义的变量必须有初始值.

int main()
{
	int a = 10;
	int& ra = a;

	auto b = 10; // int
	auto c = &a; // int*
	auto* cc = &a; // 强调只能是指针
	auto d = a;  // int&
	auto& dd = a; //强调只能是引用
	
	return 0;
}

注意事项:

  • auto*和auto&在使用上和auto本身无区别,它们表示强调定义的是指针或引用类型,其右侧紧跟的变量类型必须是指针或引用类型.
  • 当使用auto在一个语句定义多个变量时,每个等号右侧的值的类型必须相同.

7.1 auto不能推导的情况

7.1.1 auto作为函数参数时

void func(auto)
{
    //...
}
aotu不能作为形参类型,编译器无法推导出实参类型.

7.1.2 声明数组时

void func()
{
	auto b[] = { 1,2,3 };
}
auto不能直接用来声明数组.

实际上,auto的最佳搭档是范围for循环.

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

通常遍历一个数组使用的for循环我们再熟悉不过.而此循环有更强大易用的功能.

#include <iostream>
using namespace std;
int main()
{
	int a[] = { 1,2,3 };
    //只读
	for (auto e : a)
	{
		e++;
	}
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;
    //可读可写
	for (auto& e : a)
	{
		e++;
	}
	for (auto e : a)
	{
		cout << e << " ";
	}
	return 0;
}

结果:

1 2 3
2 3 4

可以这样理解基于范围的for循环:括号内的:左侧是一个容器,它存放的值的类型取决于右侧的值.:右侧表示范围中的每一个元素.而大括号内部是对容器的值进行操作,这样做的目的是保护原数据.
对数组进行遍历,其操作的权限是只读.原因是:左侧的值是由右侧拷贝而来的,为了保护原数据,即使对左侧变量操作,也不会改变原数组的值.
要修改原数组的值,就必须使用引用作为容器.然而这里使用指针是不被允许
使用范围for循环的场景:当数据类型的长度很长时.这就是范围for循环需要搭配auto使用的原因.

9. nullptr(C++11)

nullptr是C++11中的新关键字,表示指针为空.

9.1 C++98的指针空值

C++98的指针空值是延用C中的NULL.它是一个宏,通过查看头文件stddef.h

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

由此可见:NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量.这样会在某些情况出现冲突.

#include <iostream>
using namespace std;
void f(int) { cout << "int" << endl; }

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

int main() 
{
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
}

结果:

int
int
int*

第一次调用函数调用正确,但第二次传入NULL的本意是调用第二个函数,但NULL是值为0的宏.0可以是数字,也可以是无类型指针常量,但编译器默认0是一个数字(整型常量),如果要将NULL作为指针使用,必须将它强转为int*类型.
注意事项:

  • C++不再使用NULL表示空指针,进而使用nullptr.它是作为关键字引入的,所以不需要包含头文件.
  • C++11中,sizeof(nullptr)的值与sizeof((void*)0)相同

7/23/22
参考书籍:《C++ primer》
网站:维基百科
posted @ 2022-12-06 22:31  shawyxy  阅读(95)  评论(0编辑  收藏  举报