命名空间的设置:
避免冲突
命名空间:
如果将变量全部定义在全局可能不安全,都可以进行修改。
如果将变量定义在局部,当出了大括号就不能使用。
所以说在定义一个命名空间的时候
定义函数,变量,命名空间,都称为命名空间中的实体
//命名空间的定义方式
namespace hh
{
int val1 = 0;
char val2;
}// end of namespace hh
大括号后面可以加;也可以不加
定义的时候是没有缩进的,可以在最后一行添加注释使得结尾更加清晰
命名空间的使用方式:三种方式
1.作用域限定符
例如使用std命名空间中的cout 使用std::cout
准确但是繁琐
如果命名空间中套命名空间,需要 :: ::
2.using编译指令 using name space
尽量写在局部,既可以在局部(放在{}中)使用保证在用到的时候才能出来
可能会出现的问题是可能会和全局变量名出现冲突,借助于方式一作用域限定符帮助确定
当不清楚命名空间中的具体情况的时候,不要使用using,因为可能出现命名空间中的实体和自定义的变量和函数出现冲突。
3.using声明机制
初学阶段使用using声明机制,需要什么就添加什么,避免出现using冲突,同时避免每次使用加::
using std::cout;
using std::endl;
仍然是建议将using声明语句放在局部作用域中
【注意】即使命名空间中实体与全局位置实体重名,在局部位置也遵循“就近原则”形成屏蔽
#include <head.h>
int num = 10;
void test0(){
//using声明语句
using wd::num;
using wd::func;//直接写函数名,不加括号
//即使全局作用域存在同名的实体
//此处也可以形成屏蔽效果
//访问到时wd::num
cout << "wd::num:" << num << endl;
func();
}
int main(){
test0();
}
对比使用using编译机制如果全局变量和namespace中变量名字相同会出现冲突,但是使用using声明机制在局部使用的时候会对于全局变量形成屏蔽。
命名空间的嵌套使用
//嵌套命名空间设置
namespace wd
{
int num = 100;
void func(){
cout << "func" << endl;
}
namespace cpp
{
int num = 200;
void func(){
cout << "cpp::func" << endl;
}
}//end of namespace cpp
}//end of namespace wd
//调用方式
//方式一,使用作用域限定精确访问实体
void test0(){
cout << wd::cpp::num << endl;
wd::cpp::func();
}
//方式二,using编译指令一次性引入cpp的实体
void test1(){
using namespace wd::cpp;
cout << num << endl;
func();
}
//方式三,using声明语句
void test2(){
using wd::cpp::num;
using wd::cpp::func;
cout << num << endl;
func();
}
【注意】在using编译机制中,指明使用哪一个命名空间的需要具体
匿名命名空间(与有名命名空间合在一起称为有名命名空间)
匿名命名空间和static定义的静态有点像,不会被跨文件使用到
匿名空间中的变量以及函数的使用,直接用就可以不像有名空间三种方式。
【注意】全局变量和匿名空间同名的变量和函数时,也就是冲突的时候,会有问题!!
【注意】出现冲突的时候,如果使用::作用域限定符,也无法访问到匿名空间中重名的实体,只能访问到全局的实体。
跨模块调用
(1)跨模块调用函数和变量
【注意】如果是在一个文件中定义一个static变量或者函数那么也不能通过extern引入
【注意】匿名空间同理,这两类只能从本模块使用,不能跨模块使用
1. 可以定义在头文件中,是现在别的文件,然后在该文件中直接调用就可以
出现的问题就是把头文件中的内容全部引入进来,开销比较大
2. 可以使用extern的方式,
//externA.cc
int num = 100;
void print(){
cout << "print()" << endl;
}
//externB.cc
extern int num;//外部引入声明
extern void print();
void test0(){
cout << num << endl;
print();
}
告诉编译器我这有一个缺口,可以进行编译,但是谁来编译填补这个缺口,是在链接的时候确定
g++ externA.cc externB.cc
【注意】大型项目使用的方式往往还是头文件的方式实现,小型文件使用extern方式。因为跨模块调用的关系不清晰,容易出错,比如说如果说多个文件都有同一个变量会出现冲突,外部引用时
(2)跨模块调用有名命名空间
【注意】不能直接用extern wd::val直接引用,需要在该文件中依然定义一个同名命名空间,在命名空间中使用extern int val;
【注意】在不同的文件中多次定义同名命名空间,多次定义都是放在这一个命名空间中。
【注意】不能跨模块调用匿名命名空间
命名空间中的内容可以进行定义或者声明,命名空间中不能使用实体,也不能对于声明的实体赋值。
命名空间的作用:
1.避免冲突的作用
2. 版本控制,一个版本中的代码保存在一个命名空间中另一个命名空间中
3. 声明主权的作用,命名空间名字(很特别的命名方式,例如全部都是球星的名字)注释可以表明是谁的
命名空间的使用方式:
1. 最好放在命名空间中,而不是全局
2. 尽量在局部使用而不是全局
3. 如果非要使用using编译指令,建议放在所有#include预编译指令后
4. 不要在头文件中使用using编译指令,这样,使得可用名称变得模糊,容易出现二义性
【注意】头文件的使用规则include多个头文件,首先放自定义的头文件,再放C的头文件,再放C++的头文件,最后放第三方库的头文件。
const关键字
const修饰内置类型(系统原有的类型,如int,double等)
书写方式:const int num = 10; int const num = 10;对于int和double来说效果是一样的。
使用方式:const关键字必须在一开始的时候就进行赋值,也是一个变量
与宏定义的比较:1.宏定义是在预处理的时候进行替换,const是在编译的时候进行处理 2.宏定义不能明确指明类型容易出错
【重点】const修饰指令类型
对于指针而言不同的书写方式会得到不同的效果。
int num1 = 10;
const int * p = &num1;
//可以修改指向不能修改指向的内容
num1 = 20;
//这样也是可以的,用&p不行,但是直接改num1是可以的
int const * p = &num1;
//这样也是一样的效果
//point to const 就是指针指向一个const类型元素,也就是不能修改元素值
//理解为围着int转不能修改值
const int num3 = 1;
const int * p2 = &num3;
//此处设置指针的时候必须要加上const
int num2 = 1;
p2 = &num2;
//const 指针也可以修饰非常量类型数据
int num1 = 10;
int * const num = &num1;
//可以通过指针修改指针指向的内容,但是不能修改指向
//称为常量指针 const to point
//可以从右向左看,先看到const再看到*
const int num3 = 1;
int * const p2 = &num3;//error
//因为这样定义的时候会理解为可以修改p2指向的内容,但是这个地方在定义num3的时候是不能改变值
//所以这样得话是会报错的
const int num3 = 1;
const int * const p2 = &num3;
//双重const无论是是值还是指向都是不改变的
区分:数组指针和指针数组
数组指针需要注意元素个数和类型都需要进行对应
int arr[3] = {2, 3, 4};
arr 是指向第一个元素的地址
&arr是指向整个数组的指针
int *p = arr; //首元素指针
int (*p)[3] = &arr;//数组指针
//使用数组指针获取元素
for(int i = 0; i < num; i++){
cout << (*p)[i] << endl;
}
//下标的优先级是比较高的,所以说对于解引用加括号
//指针数组
//元素为指针的数组
void test5(){
int num1 = 4, num2 = 5, num3 = 6;
int * p1 = &num1;
int * p2 = &num2;
int * p3 = &num3;
int * arr[3] = {p1,p2,p3};
for(int idx = 0; idx < 3; ++idx){
cout << *arr[idx] << " ";
}
cout << endl;
}
函数指针和指针函数
void print(){
cout << "print()" << endl;
}
void show(){
cout << "show()" << endl;
}
int display(int x,int y){
return x + y;
}
//函数指针
void test6(){
//定义函数指针时要确定指向的函数类型和参数信息
void (*p)() = print;
p();
//完整写法
void (*p2)() = &print;
(*p2)();
/* p2 = display; */
p2 = show;
p2();
int (*p3)(int,int) = &display;
cout << (*p3)(4,5) << endl;
}
//指针函数
//要确保返回的指针所指向的变量的生命周期
//比函数的生命周期更长
int Num = 600;
int * f(){
return &Num;
}
void test7(){
cout << *f() << endl;
}
【注意】函数指针需要注意就是返回值的类型和参数的类型,需要是对应的
指针函数:返回值是一个指针
【注意】不能只能返回在局部定义(栈)的变量的指针会指向一个已经销毁的变量的地址,也就是确保返回的指针的声明周期比该函数的生命周期时间更长
new和delete
申请变量int char
int *p2 = new int(); // 在括号中是进行赋初值,有时会出现兼容性不强的int *p2 = new int同样的效果
cout << *p2 ; //如果没有进行赋初值的话就初始化为0,char类型是NULL
【对比】在c语言中int *p = (int *)malloc(sizeof(int)),需要说明申请大小,以及还需要进行类型的转换
1. new是一个表达式,没有括号。返回相应类型的指针,可以传参初始化也可以默认初始化为默认值
2.malloc对应相反。
借助于memcheck来确定内存损失多少内存,主要使用1,2,4情况
1)绝对泄漏了;(2)间接泄漏了;(3)可能泄漏了,基本不会出现;(4)没有被回收,但是不确定要不要回收;(5)被编译器自动回收了,不用管
间接泄漏的情况就是自定义的对象可能泄漏的情况
【注意】通过new获得内存空间也可以free释放内存,但是不匹配不要使用
申请数组空间
int * p = new int[3]();
for(int i = 0; i < 3; i++){
cout << p[i] << " ";
}
如果需要赋初值的话
int *p = new int[3]{1,2};
对于字符串的处理
char *p = new char[4]();需要多申请一个空间通过默认\0字符表示字符串的结束
const char *p = new char[4]{“hello”}; cout << p << endl;可以进行赋初值
输出流运算符对于char*指针有默认重载功能,当cout << p char指针的时候,会输出字符串。
cout其他类型的指针,会返回地址
堆上空间写入字符串方式
const char *p = "hello world!";
//放在文字常量区,默认为const,如果不使用const会报警告
char *p2 = new char[strlen(p) + 1]();
strcpy(p2, p);
cout << p2 ;
delete [] p;
回收空间的注意事项
1.申请和回收形式尽量一致,存在无法正常free的问题
malloc-----free
new--------delete
new char[5] --- delete [ ] p;
2. 安全回收指针的方式
当free的时候指针指向的内容是无效的,但也可以访问,在运行时会报错
所以说每次在使用完以后就将指针置位nullptr,更加安全,如果在访问的时候在编译时就会报错
引用(最重要)(就是对于变量进行取别名,对于两者的操作是一样的相当于作用在一个实体)
引用的底层本质就是一个指针,只是不能更改指向。
//定义方式: 类型 & ref = 变量;
int number = 2;
int & ref = number;
【注意】
1.是引用符号&,此除非取地址
2.引用类型和绑定的类型相同
3.声明引用的时候必须对于引用进行初始化,否则编译报错
一个实体可以有多个引用,也可以在引用的基础上再引用
4. 引用一经绑定,无法更改绑定。引用的地址和原数据的地址是相同的
//上面的情况就是赋值而没有进行更改绑定
引用的本质
C++中的引用本质上是一种被限制的指针。
多定义一个指针,多占据存储空间。编译器拒绝访问引用,也不能访问这个指针所在的地址只会返回本题的地址,但是实际上占据了一个指针大小的地址。
引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里,引用的本质就是“间接寻址”。
引用和指针的联系和区别
联系:都有地址的概念,引用时一个受限的指针
区别:引用必须进行初始化,引用不能修改绑定,引用表面对于引用取地址也只是可以取到变量的地址
引用的使用:引用作为函数的参数(重点)
引用传递不会发生复制,效果和指针传递效果相同,就是对于变量本身进行操作。
//例如swap函数
int swap(int & a, int & b){
int temp = a;
a = b;
b = temp;
}
问题:一个自定义的对象,如果占据空间比较大的话,就可以使用引用传递的方式。
需要改变传入参数的时候可以使用引用本身就相当于常量指针
void func(int &ref){
ref = 100;
}
//如果不想在这个函数体修改ref对应的数据
void func(const int &ref){
ref = 100;
}
当数据比较大的时候需要使用引用避免减少效率,同时又可以像值传递一样避免修改。
引用作为函数的返回值
int func(){
//...
return a; //在函数内部,当执行return语句时,会发生复制
}
int & func2(){
//...
return b; //在函数内部,当执行return语句时,不会发生复制
}
在第一种情况下是返回一个临时值,是一个右值,是不能取地址的。
但是在第二种情况下是左值,执行return 语句时并没有发生复制,返回的是绑定到本题的引用,对于本体的任何操作都可以对于引用。
【注意事项】
1. 不要返回局部变量的引用,生命周期需要比函数更长
2. 不要轻易返回一个堆上空间的指针,容易出现内存泄漏
//下述是对于堆上的空间的处理
泄漏12字节,每次调用func2就会泄漏4个字节,调用几次就new了几次。
这样话,也不能释放堆上的空间,因为需要返回一个值所以不能在函数内部delete
但是后面回收的时候就访问不到那个堆上的变量了。
可以通过这样的方式来实现使用堆上的空间:直接就是引用接收函数值
引用的作用
引用的主要作用就是用于作为参数传递的,不会产生副本并且使用const更安全,底层的实现和指针类似但是比指针更简洁。
强制转换(了解)
static_cast(四个中算是最常用的)
c语言的这种强制类型转换是不安全的
#include <iostream>
using std::cout;
using std::endl;
void test0(){
const int num = 10;
const int * p = #
/* int * p1 = (int*)p; */
/* *p1 = 100; */
//c语言的类型转换比较强大,可以将const指针转换为非const
//这个时候就可以对于数据进行改变数值
//static_cast相比于C风格的类型转换
//不安全的转换直接不允许
//1.比较安全
//2.方便查找
/* int * p2 = static_cast<int*>(p); */
}
void test1(){
int * p = (int*)malloc(sizeof(int));
*p = 1;
//<>中是目标类型
//()中是待转换的内容
int * p2 = static_cast<int*>(malloc(sizeof(int)));
*p2 = 2;
}
int main(void){
test0();
return 0;
}
c++中的这种类型转换可能更加安全一些,不需要这种情况存在,一般不会使用。
static_cast可以限制一些类型转换,可以方便查找当查找这样的关键字的时候。
实际上工作中还是使用c风格转换
const_cast(去除const属性)
如果修饰的是一个const int 类型的话,修改的是寄存器中的数据,并没有修改原本的数据
如果指针的类型是const,但是原来的数据是非const类型的,可以通过const_cast丢弃指针的const属性,可以通过指针修改数据值,实际的数据值也会发生变化。
dynamic_cast:该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中(后面讲)
reinterpret_cast:功能强大,慎用(也称为万能转换)。实际的功能强大性也比不了c风格的转换
函数重载
c语言不允许函数重载,c++允许函数重载
函数重载也就是表现就是函数名是相同的,参数类型是不一样的。需要有一点不同。
【注意】能不能重载只看参数,函数参数的数量、类型、顺序任一不同则可以构成重载。
【注意】只有返回类型不同时候,不能构成重载
这样c++对于不同类型的数据进行相同的处理的时候,可以进行函数的重载。对于数据的处理更加方便,但是在编译的时候的开销会有些大。
c语言中不允许同名函数存在,但是c++中允许,其实是对于函数的名字进行编译改编,对于不同的参数的同名函数编译为不同的名字
编译的名字中包含参数,参数不同函数名字必然不同。
在c++程序中的部分函数使用c编译(可以提升部分效率)
如果说希望在c与c++混合编译的时候,按照c的方式进行编译
可以对于单个函数使用c编译方式,也可以对于多个函数进行c语言编译方式
extern "C" void func() //用 extern"C"修饰单个函数
{
}
//如果是多个函数都希望用C的方式编译
//或是需要使用C语言的库文件
//都可以放到如下{}中
extern "C"
{
//……
}
默认参数
void func(int x = 0, int y = 0){
cout << "x = " << x << endl;
cout << "y = " << y << endl;
}
如果不传入参数,就会按照默认值进行编译,如果有传入值,就按照传入值。
可以进行缺省调用,也就是说可以传不足数量的参数
默认参数的设置顺序:
可以不设置所有的参数均为默认参数,但是必须是从右边开始设置默认值
//下述情况下相同函数名,可以输入的参数个数类型顺序都是相同的,会出现错误
void func4(int x);
void func4(int x, int y = 0);
func4(1);//error,无法确定调用的是哪种形式的func4
【注意】声明和实现中只要一个地方有默认值,但是如果说定义在后面使用在前面的时候,使用的时候不能知道有默认值
【注意】在函数重载的时候注意避免冲突
bool类型
true是1,false是0
任何类型都是可以(隐式)转换为bool类型,非0值表示true,0值表示flase
bool类型占用一个字节的空间
inline 内联函数 (编译器优化函数)
int max(int x, int y)
{
return x > y ? x : y;
}
应用的场景:
直接使用三目运算符在函数中,每次用都写容易出错,复用性不好,不够清晰。
所以将这样的一个操作封装为一个简单的函数,但是会有较大的开销,进一步的操作。
在c语言中使用宏定义来实现在预处理的时候对于相关的内容进行替换但是非常容易出错,c++中可以借助于inline函数来实现。
【注意】使用c语言中的宏定义的方式容易出错
#define MAX(a, b) (a) > (b) ? (a) : (b)
int result = MAX(20,10) + 20//result的值是多少?20
int result2 = MAX(10,20) + 20//result2的值是多少?40
//result = MAX(i, j) + 20; 将被预处理器扩展为: result = (i) > (j) ?(i):(j)+20
//即使都加上括号,也不能得到想要的结果
#define MAX(a, b) (((a) > (b)) ? ((a) : (b)))
int i = 4,j = 3;
result = MAX(i++,j);
cout << result << endl; //result = 5;
cout << i << endl; //i = 6;
我们想要得到4,5
//使用MAX的代码段经过预处理器扩展后,result = ((i++) > (j) ? (i++):(j));
i++被执行了两次
inline 函数相较于宏函数定义会有安全性检测可以进行调试,相较于函数调用是直接进行替换(底层是发出内联建议,检查有没有正确然后进行内联替换)开销是比较小的。
内联函数:
内联底层是发出一个内联建议,如果说接受inline建议,在编译的时候展开(宏预处理会有很多的问题)。
内联函数的调用部分代码要求少一些,执行代价比较大的时候不建议使用内联也就是不在乎调用的一点开销影响整体的效率
内联函数代码量小,执行的代价比较小,调用的次数比较多的时候。节省调用的开销。
endl就是一个内联函数
声明和实现可以分开写,下列两种方式均可。
但是声明和实现都必须在头文件中,因为内联的实现是在编译阶段,如果实现是在实现文件中需要在链接的时候才能进行。
so inline 应该被经常使用,即拥有宏的效率,也拥有安全性。
异常处理(了解)(不会使用,分割代码异议不明)
C++ 异常处理涉及到三个关键字:try、catch、throw.
double division(double x,double y){
if(y == 0){
throw "Deivision by zero";
}
return x/y;
}
void test0(){
double x = 100, y = 0;
try{
cout << division(x,y) << endl;
cout << "hello" << endl;
}catch(const char * msg){ //catch的小括号里是类型
cout << "hello," << msg << endl;
}catch(double x){
cout << "double" << endl;
}catch(int x){
cout << "int" << endl;
}
}
当出现异常的时候,就不会在执行cout
内存布局(重要)
【注意】由于编译器的优化,在通过代码进行检测时,显示出来的也是先写的先分配在栈的低地址段
文字常量区 const char *p就是存放在文字常量区,如何去获取存储的位置,因为使用cout输出一个char指针的时候会直接输出该处的内容而不是输出地址可以使用printf打印%p获取地址,也可以将const去除然后转换为int指针类型
获取程序代码的地址通过printf(%p,&main)实现
c风格字符串
char str[6] = {'h','e','l','l','o'};
char str[5] = {'h','e','l','l','o'};
char str[6] = {'h','e','l','l','o','\0'};
//第一种(会自动添加终止符)和第三种都是可以的,第二种会导致没有终止符的存在
char str[5] = "hello";
hello是存在在文字常量区,char str中的内容是存储在栈区的
char * p = "hello";
是直接存储在文字常量区
对于堆区进行存储字符串
char *p1 = new char[6]();
strcpy(p1, p);
const char *p1 = new char[6]{'h','e','l','l','o'};
//???????????????????
const char *p2 = "hello";
const char *p1 = new char[6]{};
想要在堆上定义一个const类型字符串;