目录
cout << "hellow world" << endl;
C++入门
1.命名空间
在c/c++中存在大量的变量、函数、类等作为全局变量,在定义和使用时,经常容易出现名字冲突。例如:
#include<stdio.h>
#include<stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
在这一段代码中就出现了重定义,rand是在头文件stdlib.h里面的函数rand(),但是我们后面又定义了全局变量rand。这样子就出现了命名冲突,也可以是函数重定义。就出错了。
在一个项目工程里,不同的人负责不同的模块,就容易出现命名冲突,如果每个人都有自己的命名空间在使用时就可以很好的避免这个麻烦。
1.1命名空间的定义
命名空间就相当于给里面的变量,函数一个定义域。
namespace 名字{ }
名字是自己定义的,就像函数名,变量名一样。{ }里面是命名空间的成员。
namespace kele
{
int rand = 10;
int ADD(int x, int y)
{
return x + y;
}
}
性质一:命名空间允许嵌套
namespace kele
{
int rand = 10;
int ADD(int x, int y)
{
return x + y;
}
namespace coco
{
int x = 0;
}
}
性质二:同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
1.2命名空间的使用
命名空间使用一有三种方式:
1.加命名空间名称及作用域限定符
#include<stdio.h>
#include<stdlib.h>
namespace kele
{
int rand = 10;
}
int main()
{
printf("%d\n",kele::rand);
return 0;
}
在每次使用这个变量或者函数时都要加速命名空间名称和作用域限定符::
2.使用using将命名空间中某个成员引入
#include<iostream>
using namespace std;
namespace kele
{
int rand = 10;
int ADD(int x, int y)
{
return x + y;
}
namespace coco
{
int x = 0;
}
}
using kele::coco::x;
int main()
{
cout << x << endl;
return 0;
}
在使用这个方式引入某个变量或者函数后,接下来在需要使用它的场景,直接用变量名或者函数名即可。
3.使用using namespace 命名空间名称 引入
using namespace kele;
int main()
{
cout << coco::x << endl;
return 0;
}
相当于直接展开这个命名空间,里面的函数和变量的定义域都被展开了,成为全局的。
2.C++的输入和输出
1.内容
下面写一段C++的输出hello world
#include<iostream>
using namespace std;
int main()
{
cout << "hellow world" << endl;
return 0;
}
我们这里不急一点点来
#include<iostream>
这里我们我们应该可以猜出来,这和c语言相似,是包头文件,iostream,我们可以把它拆为io和steam,stream在c语言应该不陌生,这是流的概念。(流,就像把数据放在一条水流里,把通过键盘输入的数据,放在流里面,流向电脑的其他设备,比如内存,然后在流向输出设备,比如屏幕,音响,硬盘...我自己的大致理解)
这里不像c语言,头文件需要加.h,c++为了区分,不加这个后缀。
io其实分别对应cin和cout的istream和ostream。
using namespace std;
std是一个命名空间,这个命名空间的定义是在头文件里的,命名空间包含了cin、cout、endl...
在使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件 以及按命名空间使用方法使用std。
cout << "hellow world" << endl;
cout是表示输出,<<是流插入运算符,>>是流提取运算符,需要输出什么就<<后面跟输出对象," "里面是字符串,endl是特殊的C++符号,表示换行输出,包含在头文件中。
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。 C++的输入输出可以自动识别变量类型。
2.注意
std命名空间的使用惯例: std是C++标准库的命名空间,如何展开std使用更合理呢?
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对 象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模 大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
3.缺省参数
3.1缺省函数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。
例如:
void Function(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Function();
return 0;
}
结果:
相当于在定义函数的参数 时提前给定一个值,如果在调用函数时没有传值,就使用定义函数时给的那个值,反之,就使用传的值。
在之前学习c语言的过程中是否出现过,需要一个数组但是不确定数组的大小,需要键盘输入大小后,可能是4个也可能是1000个,我们一般使用realloc函数不断扩容。我们是不是可以建立一个函数建立数组,同时如果确定大小,就用确定的这个数建数组,如果不确定,就先用4个再逐步加。
typedef struct SepList
{
int* arr;
int capacity;
}SL;
void SListInit(SL* sl, int n = 4)
{
assert(sl);
int* newlist = (int*)malloc(sizeof(int) * n);
assert(newlist);
sl->capacity = n;
sl->arr = newlist;
}
int main()
{
SL sl;
SListInit(&sl,5);
return 0;
}
3.2缺省参数的分类
1.全缺省参数(如上)
2.半缺省参数
void Function(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
1. 半缺省参数必须从右往左依次来给出,不能间隔着给
错误示范:
2. 缺省参数不能在函数声明和定义中同时出现 (一般在声明中出现就可以)
错误示范:
3. 缺省值必须是常量或者全局变量
4. C语言不支持(编译器不支持)
5. 赋值传值是从左往右
4.函数重载
1.函数重载概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
2.函数重载原理——名词修饰
1. 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们 可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标 文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么 怎么办呢?
2. 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就 会到b.o的符号表中找Add的地址,然后链接到一起。
3.那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的 函数名修饰规则。
在gcc编译条件下(C语言)函数名不变,并没有什么区别化的修饰。但在g++编译条件下(C++)函数名的修饰是有特殊规则的。
在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参 数类型信息添加到修改后的名字中。_Z+函数名长度+函数名+参数类型的首字母。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修 饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办 法区分。
5.引用
引用很有意思啊,在C中与之对应的就是指针。C++设计师应该是觉得C语言里的指针过于复杂,而且有野指针和空指针的问题,所以引用就来了。
1.&如何使用
类型& 引用变量名(对象名) = 引用实体
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用的变量开辟内存空间,它和它引用的变量共用同一块内存空间。
int a = 0; 定义一个整型类型的变量
int& ra = a; 相当于给a变量取了另一个名字ra,这样子a变量就有了两个名字。
2.引用特性
1. 引用在定义时必须初始化
指针定义可以不初始化,但引用定义必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
&b = c;这里报错左值不可修改 b = c 其实是变量a/b的赋值
4.引用类型必须和引用实体是同种类型的
a为常变量,也就是常量是不可以被改变的,不能作为左值,类型是const int。如果用int&来引用,那么ra对于变量的使用权限被放大了,这是不合适的。应使用 const int& 。
10是常量,也不能作为左值,使用 const int& 合适只读。
这个比较有意思,并不是因为 int 类型和 double 类型不同的原因。观察第三行。
C语言有两种类型转换,一种强制类型转换(强制转换的类型)变量名,另一种自动转换类型
在类型转换过程是产生一个临时变量,这个临时变量是a的double类型,由计算机控制,ra是作这个临时变量的别名,所以只读,没有控制权。
这里和上面的情况类似,根据函数栈帧的理解,函数在被调用之后 ,会在return一个4个字节int类型的临时变量放在寄存器中(比较小的情况)。int& a 其实是在给临时变量取别名,所以只读可以,没有控制权。
3.使用场景
1.做参数
2.做返回值
这里n是全局变量,函数销毁,n还在,函数的返回值就是n的名字
int& count = Count() 等价于 int& count = n
例子:
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;
return 0;
}
这里返回的是n,是函数内的局部变量,函数销毁后,局部变量就被销毁了,返回c的别名,如果通过c的别名ret进行操作是非法的。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
4.传值、传引用效率比较
传引用效率比传值快
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直 接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效 率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
5.引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节) 比特就业课
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
6.内联函数
1.概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率。
int ADD(int a, int b)
{
return a + b;
}
int main()
{
int ret = ADD(1, 2);//没有使用内联函数
return 0;
}
函数调用都是要 call到 函数地址 然后再 jump 到函数;
inline int ADD(int a, int b)
{
return a + b;
}
int main()
{
int ret = ADD(1, 2);//
return 0;
}
在定义函数的位置前加 inline 后,就没有call了,而是直接在这个位置执行函数的内容。
2.特性
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用,缺陷:可能会使目标文件变大(变大的内容是代码的数量),优势:少了调用开销,提高程序运 行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建 议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址 了,链接就会找不到。
Func.h
#program once
inline int ADD(int a, int b);
Func.cpp
#include"Func.h"
inline int ADD(int a, int b)
{
return a + b;
}
main.cpp
#include"Func.h"
int main()
{
int ret = ADD(1, 2);//
return 0;
}
报错原因:
如果编译器将函数当成内联函数处理,是在编译阶段,把函数体展开,在需要调用的位置。刚刚也看到了,在底层汇编里是没有call的。
其实在inline内联函数里,它定义的函数是不给函数地址的,因为它的使用环境是需要的位置展开就可以。
inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名
main.cpp的ADD是0(因为只有声明,没有定义),在链接的时候就会报错,找不到ADD。
宏的优缺点?
优点:
1.增强代码的复用性
2.提高性能
缺点:
#define ADD(x, y) ((x) + (y))
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用
3.没有类型安全的检查
C++有哪些技术替代宏?
1. 常量定义 换用const / enum
2. 短小函数定义 换用内联函数
7.auto关键字(C++11)
随着程序越来越复杂,类型的名字也越来越复杂,类型难于拼写,含义不明确也容易导致出错。我们之前学习的typedef可以将复杂的类型重命名,但typedef也会遇到新问题:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
第一个失败,第二个成功
//你以为的
const char* pt;
const char** pt1;
//实际上的
char* const pt;
char* const* pt1;
const修饰规则
1.常量本身必须初始化,因此对于本身是常量的指针,必须初始化,比如int* const p=&a;
2.对于指针本身不是常量,但是指向的对象是常量的指针,可以不初始化,比如const int *p、int const *p;
那有什么更好的方式吗?
1.auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的 是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto的使用方式和其他类型一样。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
typeid( 变量名 ).name 是用来以字符串的形式返回变量的类型
【注意】 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
2.auto的使用细则
1. auto与指针和引用结合起来使用
int main()
{
int x = 0;
auto* a = &x;
auto& b = x;
auto c = &x;
return 0;
}
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2. 在同一行定义多个变量
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能推导的场景:
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
8. 基于范围的for循环(C++11)
1.范围for的语法
原先的遍历:
int main()
{
int arr[] = { 1,2,3,4,5,6,6 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
现在的方法:
int main()
{
int arr[] = { 1,2,3,4,5,6,6 };
for (int x : arr)
{
cout << x << " ";
}
cout << endl;
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因 此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
int arr[] = { 1,2,3,4,5,6,6 };
for (int x : arr)
{
cout << x << " ";
}
cout << endl;
for (auto x : arr)//x只是arr里的变量的赋值临时变量
{
cout << x << " ";
}
cout << endl;
for (auto& x : arr)//这里x用引用就是别名可以更改arr数组里的数据
{
x *= 2;
cout << x << " ";
}
cout << endl;
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2.范围for的使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
这里的array不是数组名,而是数组指针。
2. 迭代的对象要实现++和==的操作。(还没学,后面补)
9.指针空值nullptr(C++11)
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL在C被定义为字面常量0,在C++被定义为无类型指针(void*)的常量。
在C++中最好使用nullptr来表示空指针
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。(在win32中4个字节,在64里8ge字节)
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。