C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C++学习有一定的帮助,本章节主要目标:
在C语言中我们的变量,函数名我们个人通常不会出现冲突但与他人进行合作时则有几率出现冲突从而影响所以C++*里使用了命名空间来对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
#include
#include
int main()
{
//在包含了stdlib.h这个头文件后rand就变成了随机函数
int rand = 10;
//在C语言中我们无法解决这个问题只能更换变量名
printf("%d ", rand);
//运行后会进行报错,因为rand进行了重定义
return 0;
}
而在C++中命名空间的出现就解决了这些问题
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
//命名空间的定义需要使用namespace这个关键字
//在关键字后的是命名空间的名字
namespace ly
{
//命名空间里可以定义变量,函数,结构体等
int rand = 10;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
};
2.//命名空间也进行嵌套但是不宜嵌套过多原因在后会进行解释
//test.cpp
namespace ly
{
int rand = 10;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//命名空间的嵌套
//两个不同的命名空间里的各类变量或函数名可以相同
namespace ly2
{
int rand = 10;
}
};
//3.在同一个工程中名字**相同**的命名空间会在运行时**合并**
//test.h
//这里的test.h的命名空间ly会与test.cpp里的命名空间ly进行合并
namespace ly
{
struct SList
{
int* next;
int date;
}SList;
};
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
而命名空间里的成员该如何使用呢?
namespace ly
{
int rand = 10;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
namespace ly2
{
int rand = 10;
}
};
//1.加命名空间名称及作用域限定符
int main()
{
printf("%d ", ly::rand);
return 0;
}
//这其中的ly为命名空间名称而::即为作用域限定符
//2.将命名空间全部展开
using namespace ly;
//ps:全部展开仍会带来问题,我们之后在输入输出函数时解答
int main()
{
printf("%d ", rand);
return 0;
}
//3.将命名空间进行部分展开
using ly::rand;
int main()
{
printf("%d ", rand);
printf("%d ", ly::ly2::rand);//此行代码会出现报错
return 0;
}
注意:上面的代码也为我们展现了嵌套的命名函数要如何使用,所以当我们嵌套过头时作用域限定符会过长从而不便于我们使用,解决发法在后我们也会学习到但是仍不建议各位嵌套过多层。
当C++这个新生儿出生时,他应向世界打招呼。而作为基于C语言进行改进的语言他也对于C语言的输入输出方式进行了改变,现在来让我们看看它是如何hello world的!
#include
using namespace std;
// std是C++标准库的命名空间名
//C++将标准库的定义实现都放到这个命名空间中
int main()
{
cout << "hello, world!!" << endl;
return 0;
}
说明:
而cout和cin的出现让C++相对与C语言的输入输出正常使用时方便了许多。
int main()
{
int a = 10;
double b = 1.1;
char c = 'C';
cout << a << endl;
cout << b << endl;
cout << c << endl;
printf("%d\n", a);
printf("%f\n", b);
printf("%c\n", c);
//C++不仅兼容C语言的输入输出
//并且不需要表明变量的类型因为cout会自动进行识别并打印
return 0;
}
注意:从运行结果来看,对于如控制浮点数输出精度,控制整形输出进制格式等中cout无法做到像printf那么精准,但学习C++并不是让你遗忘C语言,所以遇到这些情况时我们一样可以使用C语言来解决问题毕竟C++是兼容C语言的
注意2:在我们日常使用命名空间std时可能不会出现一些问题,但当我们的代码量增加并变得更加复杂时问题就会出现。如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。
所以我们在日常使用中直接使用 using namespace std 是几乎不会出现问题的,但是以后工作进行项目开发时建议还是进行单个对象的展开即像std::cout一样
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include
using namespace std;
void Func(int a = 1)//1即为a的缺省值
{
cout << a << endl;
}
int main()
{
Func();//运行结果是打印出1
Func(10);//运行结果是打印出10
return 0;
}
缺省函数分为全缺省与半缺省,全缺省即为每个形参都具有缺省值而版缺省则是部分参数具有缺省值。
注意:这里的半不是指一半的参数
void Func1(int a = 1,int b = 0)//全缺省
{
cout << a << endl;
cout << b << endl;
}
void Func2(int a , int b = 10)//半缺省
{
cout << a << endl;
cout << b << endl;
}
int main()
{
Func1();
Func2(10,);
return 0;
}
注意:
1.无论是全缺省还是半缺省,缺省值都应该从左向右依次给出像是Func1( ,10)还是Func2( , )都是错的。缺省值是不能间隔着给出的
2.并且缺省参数同时不能在声明和定义中同时出现
//test.h
void Func(int a = 10);
// test.cpp
void Func(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同
//那编译器就无法确定到底该用那个缺省值。
中华文化博大精深,我们在网络相比都听说过一个,说的是外国人听一段中文并判断这段中文中的”什么意思“是什么意思。而一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
#include
using namespace std;
//1.参数个数不同
void Func(int a)
{
cout << "Func(int a)" << endl;
}
void Func()
{
cout << "Func()" << endl;
}
//2.参数类型不同
void Add(int a, int b)
{
cout << "Add(int a, int b)" << endl;
}
void Add(double a, double b)
{
cout << "Add(double a, double b)" << endl;
}
//3.参数顺序不同
void Swap(double a, int b)
{
cout << "Swap(double a, int b)" << endl;
}
void Swap(int a, double b)
{
cout << "Swap(double a, int b)" << endl;
}
//void Swap(double b, int a)
//{
// cout << "Swap(double a, int b)" << endl;
//}
//注意:这里的顺序并不是形参的顺序不同,是两个形参的类型顺序不同
int main()
{
Func(10);
Func();
Add(1, 1);
Add(1.1, 1.1);
Swap(1.1, 1);
Swap(1, 1.1);
return 0;
}
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
这是C语言中想要打印出"Hello,World"所需要的阶段图解
gcc -E HelloWorld.c预处理:加入头文件,替换宏。
gcc -c -S HelloWorld.c编译:包含预处理,将C程序转换成汇编程序。
gcc -c HelloWorld.c汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
gcc -o HelloWorld.c链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。
那为什么C语言不支持函数重载而C++支持呢?
1.实际项目通常是由多个头文件和多个源文件构成,当我们知道当前a.cpp中调用了b.cpp中定义的Add函数时,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
2.所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
那编译器会如何在b.o的符号表里找到Add的地址呢,不同的编译器对于函数名的修饰规则是不同的
int func1();
void func2(char* const);
void func3(const Test& t);
void useFunc1()
{
func1();
}
void useFunc2()
{
func2("aaa");
}
int main()
{
useFunc1();
useFunc2();
return 0;
}
//func3被修饰为:?func3@Test@@QAEXABV1@@Z
//func1被修饰为:?func1@Test@@AAEHXZ
//func2被修饰为:?func2@Test@@IBEXPAD@Z
大家可以去用Linux编译尝试会发现相对在windows里的vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。
3.通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
4.而如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如在《水浒传》中有梁山108好汉,里面每个人都有着别名,例如吴用也别叫做智多星,鲁智深被叫做鲁和尚
#include
using namespace std;
void Func1(int a)
{
int& ra = a;//定义引用的类型
cout << &a << endl;
cout << &ra << endl;
//分别打印出a和ra的地址
}
int main()
{
Func1(10);
//a和ra的地址是相同的所以他们使用的是同一块空间
return 0;
}
注意:引用类型必须和引用实体是同种类型的
#include
using namespace std;
int main()
{
int a = 10;
//int& ra;编译出错,引用类型必须初始化
int& rra = a;
int& rrra = a;
cout << a << endl;
cout << rra << endl;
cout << rrra << endl;
return 0;
}
#include
using namespace std;
int main()
{
int a = 10;
int& ra = a;
int& rb = a;
cout << a << endl;
cout << ra << endl;
cout << rb << endl;
return 0;
}
#include
using namespace std;
int main()
{
int a = 10;
int b = 1;
int& ra = a;
//ra = b;
//ra无法再引用b
cout << a << endl;
cout << ra << endl;
return 0;
}
当一个变量被const修饰之后,就必须在引用的前面的也加一个const来修饰即常引用
#include
using namespace std;
int main()
{
const int a = 10;
//int& ra = a;//编译出错,变量a为为常量
const int& ra = a;//常引用
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
return 0;
}
#include
using namespace std;
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
#include
using namespace std;
int& Func(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int ra = Func(10,10);
cout << ra << endl;
int& rra = Func(10, 20);
Func(20, 20);
cout << rra << endl;
return 0;
}
注意:这里可以发现在用一个引用rra来接受Func的返回值后再次调用这个函数,rra的值同样会改变。为什么会这样呢?这就牵涉到了栈帧和内存的问题,简单来说就是第一次调用Func函数后栈帧为它开辟的空间被回收,但是仍存在所以那时候rra指向是一块被回收的空间。而第二次调用Func函数后c的值被修改成了40即使函数空间被回收了但是作为c的别名rra的值同样也会改变。
不同:
1.引用只是一个别名,而指针则是一个实体
2.引用无法指向别的实体,而指针可以改变指向的实体
3.引用不会开辟空间,而指针会
4.sizeof引用会计算它所指向的那个变量的大小,而sizoef指针则会计算指针本身的大小
5.引用使用时必须初始化,而指针不用
6.没有空引用但有空指针
相同
1.引用和指针都是使用的地址的概念,引用的底层逻辑同样也是指针,指针是在其空间内存了一块地址而引用就是某个内存的别名。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
#include
using namespace std;
inline int Func(int a,int b)//内联函数
{
int c = a + b;
return c;
}
int main()
{
int a = Func(1, 2);
cout << a << endl;
return 0;
}
//test.h
#include
using namespace std;
namespace ly
{
struct SList
{
int* next;
int date;
}SList;
};
inline int Func(int a, int b);
//test.cpp
int Func(int a, int b)
{
int c = a + b;
return c;
}
//main.cpp
#include "test.h"
int main()
{
int ra = Func(10, 20);
cout << ra << endl;
return 0;
}
//无法解析的外部符号 "int __cdecl Func(int,int)" (?Func@@YAHHH@Z),函数 _main 中引用了该符号
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
#include
using namespace std;
namespace ly
{
namespace ly2
{
namespace ly3
{
int a = 10;
};
};
};
int main()
{
cout << ly::ly2::ly3::a << endl;
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。简单来说编译器会自动识别变量的类型。
#include
using namespace std;
int Func(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = 10;
auto b = 1.1;
auto c = 'c';
auto d = Func(1, 2);
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
注意:
1.auto还可以和我们之前学的指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
#include
using namespace std;
int main()
{
int a = 10;
auto b = &a;
auto* c = &a;
auto& d = a;
cout << &a << endl;
cout << b << endl;
cout << c << endl;
cout << &d << endl;
return 0;
}
这其中的b和c则是被auto识别成了相同的类型,都是int*所以他们两想要打印地址不需要加上取地址符号。
2.auto定义一行多个变量时
当auto定义一行多个变量时要保证这几个变量的类型全部相同不然会出现编译错误
#include
using namespace std;
int main()
{
auto a = 1, b = 2;
auto c = 1.1, d = 'd';//对于此实体“auto”类型是 "char",但之前默示为 "double"
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
1.auto无法作为参数来使用
#include
using namespace std;
int Func(auto a, int b)//参数不能为包含“auto”的类型
{
int c = a + b;
return c;
}
int main()
{
Func(10, 20);
return 0;
}
2. auto不能直接用来声明数组
#include
using namespace std;
void TestAuto()
{
int a[] = { 1,2,3 };
auto b[] = { 4,5,6 };//“auto”类型不能出现在顶级数组类型中
}
int main()
{
TestAuto();
return 0;
}
在我们学习C语言的时候如果我们需要遍历一个数组则需要这样写
#include
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
{
printf("%d ", array[i]);
}
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9 };
for (auto e : array)
{
cout << e << " ";
}
return 0;
}
void TestFor(int array[])//这里的范围for就无法使用
{
for (auto e : array)//此基于范围的“for”语句需要适合的 "begin" 函数,但未找到
{
cout << e << " ";
}
}
2.范围for循环暂时只能从头迭代到尾无法像for循环一样判断==的条件
在良好的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*)0)**所以在使用中不可避免的会出现一些错误例如:
#include
using namespace std;
void test(int* a)
{
cout << "test(int* a)" << endl;
}
void test(int a)
{
cout << "test1(int a)" << endl;
}
int main()
{
int a = 10;
test(NULL);//test1(int a)
test(1);//test1(int a)
test(&a);//test(int* a)
return 0;
}
我们的本意是想用NULL来调用test(int* a)这个函数但是由于NULL被定义成了常量0结果调用成了另外一个函数。
由于NULL的不确定指向性所以在C++11中定义了一个新的空指针即nullptr,这个空指针的使用和以前我们在C语言中使用NULL是完全相同的。
注意:
C++的入门繁杂且需要更改C语言的习惯,因为祖师爷将C语言中一些他用不惯或者觉得不顺心的地方进行大刀阔斧的优化从而产生了C++但也为我们这些初学者造成了困难。在熟悉了C语言的模式后,我们应该把初期的相关细节全部缕清才能更好的跨入C++的大门。本篇已经将C++入门的相关细节全部罗列出来但仍需要大家熟悉并上手敲敲才能更好的理解其中的含义。