C++入门
前言:在我们学习完C语言之后,开始学习C++。在C++入门学习中,我们会了解到C语言的一些不足之处,和在C++中是如何来解决此类问题的。
一.命名空间
在学习C语言的时候,我们都知道不能使用关键字作为变量名来使用。如果我们在不知情的情况下使用了可能会遇到下面这种情况。
#include <stdio.h>
#include <stdlib.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
//这里将报错
//因为我们定义的变量rand和头文件中包含的函数名rand重定义
因为在C/C++中的变量,函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。因此在C++中为了解决此类问题提出了命名空间的概念。用来避免命名冲突或命名污染问题。
常见的命名冲突:
- 包含头文件,可能和库里面发生冲突 。
- 将自己写的代码和别人的代码进行合并的时候,可能会发生冲突
1.1域
在C语言中,就提到了作用域这一概念。
int a = 1; //全局域
int main()
{
//局部域
int a = 0;
//优先访问局部域,局部域中没有,在访问全局域
printf("%d\n", a);
//我们可以使用域操作符(::)两个冒号来指定访问那个域
// ::前面是空白,就默认是全局域(前面有无空格不影响)
printf("%d\n", ::a);
return 0;
}
1.2命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
int a = 1;
//命名空间域
namespace lxl
{
int a = 2;
}
int main()
{
int a = 0;
printf("%d\n", a);
printf("%d\n", ::a);
//如果我们不指定访问命名空间域或者展开命名空间域
//默认是不会去命名空间域中查找的
return 0;
}
//1.命名空间中可以定义变量/函数/类型
namespace lxl
{
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;
}
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
1.3命名空间的三种使用方式
1.3.1指定访问
- 加命名空间名称及作用域限定符
namespace lxl
{
int a = 0;
int b = 1;
int c = 2;
}
int main()
{
//域操作符(::)两个冒号
printf("%d\n", lxl::a);
return 0;
}
1.3.2部分展开
- 使用using将命名空间中某个成员引入
using lxl::b;
int main()
{
printf("%d\n", b);
return 0;
}
1.3.3 全部展开
- 使用using来展开命名空间
using namespace lxl;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
1.4展开命名空间的注意事项
命名空间全部展开之后,命名空间中的变量,函数,和类型等,将全部暴露在全局中。
- 可能会和库/和别人的代码发生冲突
- 如果展开后的命名空间中的变量和全局变量名一样,将会报错 a不明确(相当于有两个相同的全局变量)
1.5std命名空间的使用惯例
std是C++标准库的命名空间。那我们使用的时候有两种使用使用方法。一种是全部展开,一种是部分展开。两种展开的区别大家都已经了解,那什么场景中应该使用哪种方式?
- 在日常练习中,建议直接using namespace std即可,这样就很方便。
- 在一些项目中,建议展开常用的库对象/类型等方式。
1.6命名空间的合并
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合并成同一个命名空间中。
注意:
- 如果两个命名空间中的变量名相同,将会报错。
二. C++输入&输出
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
//using namespace std;
using std::cout;
using std::endl;
using std::cin;
int main()
{
int a = 0;
double b = 0;
char c = 0;
char d[] = "hello world!!";
cin >> a >> b >> c;
cout << a << ' ' << b << ' ' << c << endl << d;
return 0;
}
2.1cout & cin
cout就相当于C语言中的Printf,cin就相当于scanf。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
2.2<< & >>
<<是流插入运算符,>>是流提取运算符.
比较:
- 使用printf/scanf时需要手动控制格式
- C++的输入输出可以自动识别变量类型。
三.缺省参数
3.1概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值(默认值)。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include <iostream>
using namespace std;
int Add(int x = 3, int y = 2)
{
return x + y;
}
int main()
{
int sum = Add(); //没有传参使用默认的缺省值
int sum2 = Add(10, 20); // 传参时,使用指定的实参
printf("%d\n", sum);
printf("%d\n", sum2);
return 0;
}
3.2缺省参数分类
- 全缺省:函数的每一个参数都指定一个缺省值
int Add(int x = 3, int y = 2)
{
return x + y;
}
- 半缺省:函数的参数没有全部被指定一个缺省值。
int Add(int x , int y = 2)
{
return x + y;
}
int main()
{
int sum = Add(3);
return 0;
}
3.3注意事项
指定缺省值时,我们应该注意,声明和定义不能同时给缺省值(会被认为重定义)。
- 缺省参数不能在函数声明和定义中同时出现,建议声明给缺省值,定义不给缺省值。
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
- 缺省值必须是常量或者全局变量
3.4缺省参数解决的问题
在C语言中,我们不能像C++一样可以设置缺省值。因此在一些地方用起来不方便。列如:我们现在写一个顺序表,当数组的空间不够的时候我们可以扩容。但是当使用的人不给出数组的空间的时候,我们最开始给这个数组多大空间呢?多了浪费,少了需要扩容。因此,我们就可以设置一个缺省值。
四.函数重载
4.1概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
注意:如果只有返回值不同,不会构成函数重载。
int Add(int a = 3, int b = 4)
{
return a + b;
}
double Add(double a = 3.0, double b = 4.0)
{
return a + b;
}
int main()
{
int sum = Add(2, 3);
double dsum = Add(1.1, 2.2);
cout << sum << endl;
cout << dsum << endl;
return 0;
}
这里大家是否有疑惑,为什么编译器可以根据形参列表的不同,而去调用对应的函数?
这是因为:函数重载可以自动识别参数的类型。 关于为什么可以自动识别参数类型,就必须了解C++中对函数名的修饰。
五.函数名修饰
在不同的平台,函数名的修饰规则不同。这里用Linux g++的命名规则举列子。
//Linux:_Z3Addii
// 3: 函数名的长度
// Add: 函数名
// ii: 形参的类型
int Add(int a = 3, int b = 4)
{
return a + b;
}
而在C语言中,是直接用函数名,因此不能定义两个函数名相同的函数。
六.引用
6.1概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体
int main()
{
int a = 4;
//给a取别名
int& b = a;
int& c = a;
//给a的别名取别名
int& d = b;
int& e = d;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
cout << &e << endl;
return 0;
}
注意:
- 可以给实体取多个别名,也可以给别名取别名。
- 引用类型必须和引用实体是同种类型的。
- 引用在定义时必须初始化(有实体)
- 引用一旦引用一个实体,再不能引用其他实体。
6.2引用的使用场景
6.2.1做输出型参数,提高效率
输出型参数:形参的改变会影响实参。
- C语言
void exchange1(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 3;
int b = 5;
exchange1(&a, &b);
printf("%d\n%d\n", a, b);
return 0;
}
- 引用
void exchange(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 3;
int b = 5;
exchange(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
- 结构体
typedef struct ListNode
{
int val;
struct ListNode* next;
}*PNode;
void LTPushBack(PNode& phead, int x);
- 引用做参数提高效率主要体现在大对象或深拷贝类对象的时候。
6.2.2引用做返回值
- 1.直接返回值, 会生成临时变量。
int Add(int x, int y)
{
int c = x + y;
return c;
}
int main()
{
int a = 3;
int b = 2;
int sum = Add(a, b);
cout << sum << endl;
return 0;
}
传值调用返回的时候因为函数栈帧的销毁,此时需要一个临时变量(可能是寄存器) 来拷贝C的值,最后在将临时变量的值拷贝给sum。一共需要拷贝两次。
- 2.引用做返回值,不会生成临时变量,这样可以减少拷贝次数,提高效率。
int& Func()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Func();
cout << ret << endl;
return 0;
}
此时引用做返回值,返回n的别名,因此返回的时候不会创建临时变量,在将n拷贝给ret。(拷贝一次)
- 3.获取和修改返回值
struct SeqList
{
int a[100];
size_t size;
};
int SLGet(SeqList* ps, int pos)
{
assert(pos < 100 && pos >= 0);
return ps->a[pos];
}
void SLModify(SeqList* ps, int pos, int x)
{
assert(pos < 100 && pos >= 0);
ps->a[pos] = x;
}
int& SLAt(SeqList& s, int pos)
{
assert(pos < 100 && pos >= 0);
return s.a[pos];
}
int main()
{
SeqList s;
//C方式
SLModify(&s, 0, 1);
cout << SLGet(&s, 0) << endl;
// 对第0个位的值+5
int ret1 = SLGet(&s, 0);
SLModify(&s, 0, ret1+5);
cout << SLGet(&s, 0) << endl;
//引用返回获取和修改返回值
SLAt(s, 0) = 1;
cout << SLAt(s, 0) << endl;
SLAt(s, 0) += 5;
cout << SLAt(s, 0) << endl;
return 0;
}
6.3引用做返回值的注意事项
-
不能返回局部变量
-
错误案例
int& Func(int x)
{
int n = x;
n++;
return n;
}
int main()
{
int ret1 = Func(10);
cout << ret1 << endl;
//函数栈帧销毁,此时返回n的别名,在赋值给ret1
//此时n的值是不确定的
//验证:
//函数栈帧销毁,此时返回n的别名,在给n的别名取别名ret2
//此时再次调用打印函数,之前的Func的函数空间被打印函数使用
//里面的数据发生变化,因此n的值变为随机数
int& ret2 = Func(20);
printf("xxxxxxx\n");
cout << ret2 << endl;
return 0;
}
- 正确的案例:
int& Func(int x)
{
static int n = x;
n++;
return n;
}
int main()
{
int& ret = Func(10);
printf("xxxxxxxx\n");
cout << ret << endl;
return 0;
}
因此使用引用做返回值得场景是:出了作用域之后返回的对象没有还给操作系统(static,malloc, 结构体…)。如果出了作用域之后返回值还给操作系统就只能使用传值返回。
6.4常引用
- 传值返回会创建临时变量,同时临时变量具有常属性。
正确做法:
int Func(int x)
{
static int n = x;
n++;
return n;
}
int main()
{
const int& ret = Func(10);
return 0;
}
- 发生类型转换的时候,也会产生临时变量。 相同类型不会。
正确做法:
int main()
{
double d = 3.14;
const int& a = d;
cout << a; //3
return 0;
}
为什么发生类型转换的时候会产生临时变量呢?
列如:我们需要将double类型的数据转换为int类型,此时产生一个临时变量,将double的值拷贝后,转换为int类型在赋值给int变量(如果没有临时变量,此时原double的值就会被改变)。
6.5权限
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 权限放大, a为常量
const int& ra = a; // 权限平移
int b = 10;
// int& rb = 10; // 权限放大,10为常量
const int& rb = 10; //权限平移
int c = 10;
const int& rc = c; //缩小rc作为别名的权限
c++; //c可改,rc不可改
double d = 12.34;
//int& rd = d; // 类型转换,创建临时变量,具有常属性
const int& rd = d; //权限平移
}
注意:权限可以缩小/平移,但是不能放大。
6.6引用和指针的区别
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
- 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
在学习C++前期,我们暂时认为引用是没有独立空间的就好了。
6.6.1引用和指针的不同点
- 引用概念上是变量的别名,指针则是存储变量的地址。
- 引用在定义的时候必须初始化,指针没有要求。
- 引用初始化之后就不能在引用别的实体,指针可以存储同类型的不同变量的地址。
- 没有NULL引用,但又NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(4/8)
- 引用加一则实体加一,指针加一则向后偏移一个指针类型的大小。
- 没有多级引用,有多级指针。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
七.内联函数
7.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
7.2内联函数和函数的区别
- 函数
当每调用此函数时,都会去栈上开辟空间。如果调用频繁就会有建立栈帧的开销,降低程序的运行效率。
- 内联函数
内联函数会在调用的地方展开,这样就能减少建立函数栈帧的开销,并且并且能提高程序的运行效率。
7.3内联函数的特性
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同。一般建议将函数规模小的,频繁调用的的函数采用inline修饰。对于规模较大的函数和递归编译器可能会忽略inline特性。
- inline是一种以空间换时间=的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- 列如:有10000个地方调用Func函数(50行代码)
-
- 如果直接函数调用将会产生10000(call指令) + 50 行指令。
-
- 如果内联展开将会产生10000 * 50行指令。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
7.4C语言中使用宏来解决此类问题
#include <iostream>
using namespace std;
#define Add(x, y) ((x) + (y))
int main()
{
int a = 2;
int b = 3;
cout << Add(a, b) << endl;
return 0;
}
7.5宏的优缺点
-
优点
-
- 增强代码的复用性。
-
- 提高性能。
-
缺点
-
- 不方便调试宏。(因为预编译阶段进行了替换)
-
- 代码可读性差,可维护性差,容易误用。
-
- 没有类型安全的检查 。
7.6C++中如何替代宏
- 常量定义 换用const enum
- 短小函数定义 换用内联函数
八.auto关键字(C++11)
8.1auto简介
C++11中,auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。通俗来说就是:auto可以根据右边的表达式,自动推导类型
int main()
{
int a = 10;
//根据右边的表达式,自动推导类型
auto b = a;
auto d = 1 + 1.1;
//typeid 是打印类型的函数
cout << typeid(b).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
注意:使用auto定义变量时必须对其进行初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。(因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。)
8.2auto的使用场景
- auto一般在类型很长的时候使用,来进行长类型替换。
- 类型难于拼写
- 含义不明确导致容易出错
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },
{"pear","梨"} };
//std::map<std::string, std::string>::iterator 是一个类型
std::map<std::string, std::string>::iterator it1 = m.begin();
//使用auto来进行长类型的替换
auto it2 = m.begin();
return 0;
}
8.3auto使用的细则
8.3.1auto与指针和引用结合起来使用
- 用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;
}
8.3.2同一行定义多个变量
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
auto a = 1, b = 2;
// 该行代码会编译失败,因为c和d的初始化表达式类型不同
auto c = 3, d = 4.0;
return 0;
}
8.4auto不能推导的场景
-
- auto不能作为函数的参数
-
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
//不能直接用来声明数组
auto b[] = {4,5,6};
}
九.基于范围的for循环(C++11)
9.1 范围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[] = { 1, 2, 3, 4 };
for (auto e : arr)
cout << e << ' ';
return 0;
}
若我们给数组中的元素每个乘2,又该如何写呢?
- 错误案列
int main()
{
int arr[] = { 1, 2, 3, 4 };
for (auto e : arr)
e *= 2;
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
解释:因为此时的范围for是取数组中的内容,然后拷贝给e,e的改变并不会影响数组的内容。
- 正确案列(引用)
int main()
{
int arr[] = { 1, 2, 3, 4 };
for (auto& e : arr)
e *= 2;
for (auto& e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
9.2范围for的使用条件
-
- for循环迭代的范围必须是确定的,适用于数组。 对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
解释:此时这里的array是指针,不是数组。
十.指针空值nullptr(C++11)
- 在C++98中NULL实际是一个宏,在传统的C头文件(stddef.h)中。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*) 函数,但是由于NULL被定义成0,因此与程序的初衷相。
- 在C++11中使用nullptr来表示空指针。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
注意:
-
- . 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
-
- 在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同。
-
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。