大二在学校时,学校就开设了C++这门课程,但是我并没有认真的去学习。最近重新翻开了C++的课本,才发现了C++的奇妙之处,想在此将我的学习历程记录下来。
首先我们要明白什么是C++,C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
在这里我们先不谈抽象数据类型和面向对象,因为C++是基于C语言而产生,那么在学习C++之前,我们应该先来看看C++对于C语言缺陷的一些简单的改进之处。
在C语言中,所有的变量及函数都存在于全局作用域中,可能会导致很多冲突。因此在C++中,增加了命名空间的概念,目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,命名空间是用namespace关键字来解决这个问题的。
1.1 命名空间的定义
//定义一个普通的命名空间
namespace N1 //N1为这个命名空间的名称
{
//命名空间里即可以定义变量也可以定义函数
int a = 0;
int add(int left, int right)
{
return left + right;
}
//命名空间也可以嵌套
namespace N2
{
int b = 1;
int sub(int left, int right)
{
return left - right;
}
}
}
要注意的是,定义了一个命名空间就是定义了一个新的作用域,命名空间里定义的所有内容只在该作用域里有效。如果在同一个作用域中定义了多个相同名字的命名空间,那么编译器在编译时会将这几个相同名字的命名空间合并成同一个命名空间。
1.2 命名空间的使用
命名空间的使用有三种方式:
int main()
{
//N1 是变量 a 所在的命名空间,:: 是作用域限定符
printf("%d\n", N1::a);
return 0;
}
//使用 using 将我们需要的变量引用到当前的作用域中作为全局变量使用
//同样也需要使用作用域限定符
using N1 :: a;
int main()
{
printf("%d\n", a);
return 0;
}
//使用 using namespace + 命名空间名称 可以将整个命名空间引用到当前作用域
//此时该命名空间里定义的所有内容在当前作用域都有效
using namespace N1;
int main()
{
printf("%d\n", a);
printf("%d\n", add(2, 3));
return 0;
}
首先明确,输入和输出并不是C++语言中的正式组成成分。C和C++本身都没有为输入和输出提供专门的语句结构。在C语言中,输入和输出的功能是通过调用scanf函数和printf函数来实现的,在C++中是通过调用输入输出流库中的流对象cin和cout实现的。也就是说输入输出不是由C++本身定义的,而是在编译系统提供的 I/O 库中定义的。
现在阶段我们不需要知道C++的输入和输出是怎么通过流来实现的,我们只需要知道它是怎么使用的即可。
#include
using namespace std;
int main()
{
int a;
double b;
char c;
cin >> a; //cin 可以一次输入一个数据
cin >> b >> c; //也可以一次输入多个数据,不同类型的数据也可以一次性输入
cout << a; //cout可以一次输出一个数据
cout << b << c << endl; //也可以一次输出多个数据,不同类型数据可以同时输出
//末尾加endl表示换行
return 0;
}
在使用cin输入和cout输出时,我们不用输入每个数据的类型,系统会自动判别出数据的类型,使数据按其相应的类型输入或输出。
在C语言中,当我们调用一个已经定义好的函数时,必须严格按照函数定义时的参数个数和顺序传参。而在C++中,我们可以只传部分参数就可以正确的完成一个函数的调用,这就用到了C++中的缺省参数。
缺省参数的定义如下:缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
void Test_Func(int a = 10)
{
cout << a << endl;
}
int main()
{
Test_Func(5); //传参时,使用给定的参数
Test_Func(); //没有传参时,使用默认值
}
缺省参数共分为两类:
全缺省参数是指函数在定义时每一个参数都给定了其默认值,在调用函数传参时可以不穿任何参数。
void Test_Func(int a = 10, int b = 5, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Test_Func();
}
半缺省参数是指在函数定义时并没有给所以的参数给定默认值,因此我们在调用函数时,至少要给没有默认值的参数传参。
void Test_Func(int a, int b = 5, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Test_Func(10);
}
注意:1、半缺省参数必须从右往左依次给出,不能间隔着给
2、缺省参数不能在函数声明和定义中同时出现,建议最好在声明中给出
3、缺省值必须是常量或者全局变量
函数重载也是在C++中新增的一个重要概念。在C语言中,同样函数名的函数只能定义一次,而在C++中引入了函数重载的概念,那么同样功能的函数可以起相同的名字,在调用时编译器也能很好的区分他们,下面我们来了解一下函数重载。
函数重载是函数的一种特殊情况,C++允许在同一作用域中国声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理和实现功能类似但数据类型不同的问题。
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
add(2, 3); //这两个函数虽然函数名相同,但由于参数类型不同,构成了重载
add(2.0, 3.0); //在调用这两个函数时,编译器能够通过传参的类型辨别调用哪个函数
}
当两个函数的函数命名相同,参数个数相同但类型不同时,这两个函数可以构成重载。但如果两个函数只是返回值的类型不同,那么这两个函数是不能构成重载的。因为在调用函数时,我们并不知道这个函数需要返回一个什么类型的值,并且通过参数的类型也不能区分两个函数,那么这时编译器就会报错。所以我们要注意,仅有返回类型不同的两个函数是不能构成重载的。
那么为什么C语言不支持函数重载,而C++却支持呢?让我们来看一下在C语言和C++中编译器分别是怎么对函数命名的,我们分别在C和C++环境下来运行一下这段代码,看看编译器报错时是怎么命名函数的。
int add(int a, int b);
int main()
{
add(2, 3);
}
首先是C语言环境下的:
我们可以看到,在C语言中,编译器对于函数的命名仅仅是在我们定义的函数名前加了一个下划线,那么如果我们定义多个同名函数,那么在编译器中他们的名字也是相同的,因此在调用时编译器并不能区分他们。
再来看看C++环境下的:
我们可以看到,相比C的编译器,C++在为函数命名时更为复杂,它是将我们定义的函数名和参数类型一起编进了名字中,这样当两个函数名相同时,由于参数不同,那么这两个函数在编译器中的名字也是不同的,这就是为什么C++支持重载的原因。
对于一个数据可以使用“引用”,这是C++对C的一个重要扩充。引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
int main()
{
int a = 0;
int& b = a;
printf("%p\n", &a);
printf("%p\n", &b);
}
在上述的代码中,通过 类型& 引用变量名 = 引用实体 的方式,我们声明了 b 是 a 的引用,即 b 是 a 的别名。经过这样的声明后,a 或 b 的作用相同,都代表同一变量,我们可以通过打印地址的方式来验证。要注意的是,引用类型必须和引用的实体是同一种类型的。
int main()
{
int a = 0;
//int& b; //该引用在定义时未初始化,编译时会报错
int& c = a;
int& d = a;
}
在上面的例子中我们可以看到,引用在定义时必须初始化,即声明它代表哪一个变量。一个变量可以有多个引用,但一个引用一旦引用一个实体,就不能再引用其他实体。
int main()
{
const int a = 10;
//int& b = a; //a为常量,编译器会报错
const int& b = a;
double c = 12.34;
//int& d = c; //引用类型不同,编译器会报错
const int& d = c;
}
当引用实体为常数或被const修饰时,普通的引用是不能直接引用的,编译器会告诉你这是不安全的,因为我们可以通过引用修改这个原本不能被修改的值,因此引用也必须被const修饰,才能引用为常量的实体,这通常被称作常引用。同样的,普通的引用不能直接引用于引用类型不同的实体,但加了const后就可以了,编译器会开辟为引用分配一个引用类型大小的空间,将实体中的数据拷贝过来,超过引用类型大小的部分就会被丢弃,但因为这个新的空间的地址我们是拿不到的,因此是不能被修改的,要用const修饰。
接下来我们看看引用的使用场景:
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 5;
int y = 10;
Swap(x, y);
}
用引用做参数时,相当于将参数本身传进了函数里,相比指针更加的方便简洁。
int& test(int& a)
{
a += 10;
return a;
}
int& add(int a, int b)
{
int c = a + b;
return c;
}
上述两段代码,只有第一段能正确的返回我们想要的结果,为什么呢?可以看到,第二个函数返回的是在函数内部定义的变量,而当一个函数返回时,离开函数作用域后,其栈上的空间已经还给系统,那么系统可能会将空间分配给其他地方使用,那么这个时候我们得到的返回值就不一定是我们想要的结果了,因此不能用栈上的空间作为引用类型返回值。如果以引用类型返回,返回的生命周期必须不受函数的限制,即比函数的生命周期长。
因为引用和指针在作为参数传递和作为返回值时的效果相同,那么他们的效率哪个更好呢?经过验证,不论是作为参数还是作为返回值,引用和指针的效率几乎是相同的,那么我们来看看他们的区别。
引用和指针的不同点:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有压栈的开销,内联函数能提升程序运行的效率。如果在函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
inline是一种以空间换时间的做法,省去调用函数的额外开销。所以代码很长或者有循环或递归的函数不适宜使用作为内联函数。inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环或递归等等,编译器优化时会自动忽略掉内联。内联函数不建议声明和定义分开,可能会出错,只能在当前定义的文件中使用
由于内联函数和宏特别的相似,因此我们要好好的区分内联函数和宏。
首先我们来看看宏的优缺点:
优点:1、增强代码的复用性
2、提高性能
缺点:1、不方便调试宏(因为预编译阶段进行了替换)
2、导致代码可读性差,可维护性差,容易误用
3、没有类型安全的检验
在C中也有auto关键字,它的定义是:使用auto修饰的变量,是具有自动存储的局部变脸。但因为我们平时定义的变量都会自动储存,因此auto这个关键字就显得比较多余了,也并没有人去使用它。
在C++11中,标准委员会赋予了auto新的含义:auto不再是一个存储类型指示符,而是作为一个人新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推到而得。也就是说,用auto声明的变量,用户不需要说明变量的类型,而是由编译器在编译期间由推导得知变量的类型的。
当我们使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并不是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量的实际类型。
auto的使用:
1、auto与指针和引用结合起来使用
用auto声明指针类型时,用 auto 和 auto* 没有任何区别,但用auto声明引用类型时必须加 &
int main()
{
int x = 5;
auto a = &x; //定义一个指向 x 的指针
auto *b = &x; //定义一个指向 x 的指针
auto& c = x; //定义一个引用类型 c 作为 x 的别名
//auto e; //未对进行初始化,会报错
}
2、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
int main()
{
auto a = 3, b = 5;
//auto c = 10, d = 0.5; //同一行定义的两个变量类型不同,编译不通过
}
auto不能使用的场景:
1、auto不能作为函数的参数
//当auto作为形参类型时,编译器无法对参数的实际类型进行推导,因此编译不通过
void Test(auto x)
{}
2、auto不能直接用来声明数组
int main()
{
int a[] = {1, 2, 3};
//auto b[3] = a; //编译不通过,auto不能用来定义数组
}
在C/C++98中,当我们初始化一个初值为空的指针时,会使用 NULL 对指针进行初始化。NULL实际上是一个宏,在传统的C头文件(stddef.h)中我们可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。
因此在C++11中给出了全新的nullptr表示空值指针,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
注意: