文章目录
第一章 C++基础 vs C
〇、C++与C对比
C面向过程 :模块化、事务逻辑顺序进行
C++面向对象 :抽象、封装、继承、多态
一、命名空间
1.概念
命名空间:所谓的命名空间就是一个可以有用户自己定义的作用域,在不同的作用域中可以定义相同名字的变量,互不干扰,系统能够区分它们。
在声明一个命名空间时,大括号内不仅可以存放变量,还可以存放以下类型:
- 变量
- 常量
- 函数,可以是定义或声明
- 结构体
- 类
- 模板
- 命名空间,可以嵌套定义
2.使用方式
命名空间一共有三种使用方式,分别为using编译指令、作用域限定符、using声明机制。
-
using编译指令
第二行指令就使用了using编译指令。(std里面全部实体引用)
#include <iostream> //c++的头文件都是用模板进行编写的,而模板的特点:是必须知道所有实现才能进行正常编译 using namespace std;//命名空间 int main(int argc,char *argv[]) { cout << "hello,world"<<endl; return 0; }
-
作用域限定符
直接使用作用域限定符==::==
namespace wd { int number = 10; void display() { //cout,endl都是std空间中的实体,所以都加上'std::'命名空间 std::cout << "wd::display()" << std::endl; } }//end of namnespace wd int main(void) { std::cout << "wd::number = " << wd::number <<endl; wd::display(); }
这种方式显得比较冗余
-
using声明机制
using声明机制的作用域是从using语句开始的,到using所在的作用域结束。
在同一作用域内用using声明不同的命名空间的成员不能有同名的成员,否则会发生重定义
(只选取std里面个别的实体引用)
#include <iostream> using std::cout; using std::endl; namespace wd { int number = 10; void display() { cout << "wd::display()" << endl; } }//end of namespace wd using wd::number; using wd::display; int main(void) { cout << "wd::number = " << number << endl; wd::display(); }
3.匿名命名空间
命名空间还可以不定义名字,不定义名字的命名空间成为匿名命名空间。
由于没有名字,该空间中的实体,其他文件无法引用,它只能在本文件的作用域中有效,它的作用域是从匿名命名空间声明开始到本文件结束
namespace
{
int val1 = 10;
void func();
}//end of anonymous namespace
在匿名空间中创建的全局变量,具有全局生存去,却只能被本空间内的函数等访问。
4.命名空间的嵌套及覆盖
int number = 1;
namespace wd
{
int number = 10;
namespace luo
{
int number = 100;
void display()
{
cout << "wd::luo::display()" << endl;
}
}//end of namespace wd
void display(int number)
{
cout << "形参number = " << number << endl;
cout << "wd命名空间中的number = " << wd::number << endl;
cout << "luo命名空间中的number = " << wd::luo::number << endl;
}
}//end of namespace wd
int main(void)
{
using wd::display;
display();
return 0;
}
二、const关键字修饰变量
1.const关键字修饰变量
const int number1 = 10; //const关键字修饰的变量称为常量
int const number2 = 20;
const int val;//(❌)error 常量必须要进行初始化
还有宏定义方式创建常量:
#define NUMBER 1024
2.const关键字修饰指针
常量指针、指针常量
int number1 = 10;
int number2 = 20;
const int * p1 = &number1;//常量指针
*p1 = 100;//(❌)error 通过p1指针无法修改其所指内容的值
p1 = &numbers;//ok 可以改变p1指针的指向
int const * p2 = &number1; //常量指针的第二种写法
int * const p3 = &number1;//指针常量
*p3 = 100;//ok 通过p3指针可以修改其所指内容的值
p3 = &number2;//(❌)error 不可以改变p1指针的指向
const int * const p4 = &number1;//两者皆不能进行修改 (const指针常量)
3.const关键字修饰成员函数
4.const关键字修饰对象
5.const关键字与宏定义的区别
const常量与宏定义的区别是什么?
1.编译器处理方式不同。宏定义是在预处理阶段展开,做字符串的替换。;而const 常量是在编译时。
2.类型和安全检查不同。宏定义没有类型,不做任何类型检查;const常量有具体 的类型,在编译期会执行类型检查。
在使用中,应尽量以const替换宏定义,可以减小犯错误的概率。
三、new/delete表达式
在C中用来开辟和回收堆空间的方式是采用malloc/free库函数。
在C++中提供了新的开辟和回收堆空间的方式,即采用new/delete表达式
1.开辟一个元素空间
int * p = new int(1);//申请空间,并初始化为1
cout << *p << endl;
delete p;//释放空间
2.开辟一个数组空间
int * p = new int[10]();//开辟数组时,要记得采用[]
for(int idx = 0; idx != 10; ++idx)
{
p[idx] = idx;
}
delete []p;//回收时,也要采用[]
new/delete表达式与malloc/free的区别是?
1)malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符或表达式 ;
2)new能够自动分配空间大小,malloc需要传入参数;
3)new开辟空间的同时还对空间做了初始化的操作,而malloc不行;
4)new/delete能对对象进行构造和析构函数的调用,进而对内存进行更加详细的工作,而malloc/free不能。
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?
因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
附:内存泄漏、内存溢出、内存踩踏、野指针的区分
区分一下概念:内存泄漏、内存溢出、内存踩踏、野指针?
内存泄漏:是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。
内存溢出:是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序要用到的内存大于能提供的最大内存。
内存踩踏:是指拷贝的目的地址和源地址有重叠。
野指针:是指指针指向的位置是不可知的,指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量。
四、引用
1.概念
在C++中,引用是一个已定义变量的别名。
语法:
类型 & 引用名 = 目标变量名;
void test0()
{
int a = 1;
int & ref1 = a;//绑定到某个变量之后,就不会再改变其指向
int & ref2;//(❌)error 声明引用的同时,必须对引用初始化;
}
==引用的底层实现:==指针 (* const ==> 指针常量)
2.引用作为函数参数
//实质:值传递->进行复制
//有函数开销(复制的时候)
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
} a
//用指针作为参数 实质:地址传递->改变地址上的变量
//开销小,函数效率较高,直观性不好
void swap(int * pa, int * pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
//引用作为参数 实质:操作变量本身
//开销小,函数效率较高,直观性好
void swap(int & x, int & y)
{
int temp = x;
x = y;
y = temp;
}
3.引用作为函数的返回值
语法:
类型 & 函数名(形参列表)
{ 函数体 }
例子:
int gNumber;//全局变量
int func1() //当函数返回时,会对temp进行复制
{
temp = 100;
return temp;
}
int & func2()//当函数返回时,不会对temp进行复制,因为返回的是引用
{
temp = 1000;
return temp; //(❌)error 不要返回局部变量的引用,局部变量在返回后被销毁
}
//不要返回堆空间的引用,可能会造成内存泄漏
//除非有内存回收的机制
int & func3()
{
int * pint = new int(1);
return *pint;
}
void test()
{
int a = 2, b = 4;
int c = a + func3() + b;//内存泄漏
cout << "c =" << c << endl;
int &ref=func3();
cout << "ref = " << ref <<endl;
delete &ref; //释放空间
}
五、C++强制转换
1.static_cast
int ->float
int iNumber = 100;
float fNumber = 0;
fNumber = (float) iNumber;//C风格
fNumber = static_cast<float>(iNumber);
void * ->其他类型的指针
void * pVoid = malloc(sizeof(int));
int * pInt = static_cast<int*>(pVoid);
*pInt = 1;
不能完成任意两个指针类型间的转换
int iNumber = 1;
int * pInt = &iNumber;
float * pFloat = static_cast<float *>(pInt);//(❌)error
用于类层次结构中基类和子类之间指针或引用的转换。
2.const_cast
该运算符用来修改类型的const属性。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;
常量对象被转换成非常量对象。
const int number = 100;
int * pInt = &number;//(❌)error
int * pInt2 = const_cast<int *>(&number);
3.dynamic_cast
该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中。
4.reinterpret_cast
该运算符可以用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。
六、函数重载
1.重载
C++支持重载,C不支持重载的原因?
C++在(g++ -c test.cc test.o)汇编的时候,会把函数的名字改编
规则:
- 函数名称必须相同 。
- 参数列表必须不同(参数的类型不同、个数不同、顺序不同)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载。
例子:
void swap(short *, short *); //swapss 名字改编
void swap(int *, int *); //swapll 名字改编
void swap(float *, float *); //swapff 名字改编
附:混合编程(c语言编译)
#ifdef _cplusplus //如果是cpp文件,执行下面内容
extern "c" //采用c语言方式进行编译
{
#endif
int add(int x, int y)
{
return x + y;
}
#ifdef _cplusplus
}//end of extern "c"
#endif
七、默认参数
//目的:默认参数就是为了少些代码
//赋值顺序:必须从右向左赋值,中间不能有缺少(先z,再y,最后X)
void add(int x=0, int y=0, int z=0)
{
return x + y + z;
}
//默认参数与函数重载时:注意函数的二义性
int add(int x, int y)
{
return x + y;
} //(❌)error 重载函数域默认参数有二义性
int main()
{
int a =3, b=4, c=5;
//若不给参数传递实参,则函数会按指定的默认值进行工作
cout << "add() = " << add() <<endl; // 0
cout << "add(a) = " << add(a) <<endl; //3
cout << "add(a,b) = " << add(a,b) <<endl; //7 此语句导致二义性
cout << "add(a,b,c) = " << add(a,b,c) <<endl; //12
}
八、bool类型
int x = true; //1
int y = false; //0
int size=sizeof(bool); //占据内存空间大小为1
九、inline函数
概念:
内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
定义函数时,在函数的前面以关键字“inline”声明函数,即可使函数称为内联声明函数。
声明和定义必须在一起
//宏定义函数
//没有函数的开销,效率相对较高,容易出错,不可调试
#define multiply(x,y) ((x)*(y))
//普通函数调用是由开销的,效率较低
int add(int x ,int y)
{
return x + y;
}
//内联函数的特点:遇到函数调用的时候,直接用函数体里面的内容去替换
//内联函数:不要有for/while等复杂的代码,否则代码膨胀
//效率较高
//inline函数必须将函数的声明和函数的定义放到一起,否则就会出错
inline int add(int x, int y) //15-18行都应在头文件中,头文件中不能只声明
{
return x + y;
}
十、异常安全
c++异常处理涉及到三个关键字:try、catch、throw
语法
try
{
throw 表达式;
//语句块
}
catch(异常类型)
{
//具体的异常处理...
} ...
catch(异常类型)
{
//具体的异常处理...
}
throw
表达式
抛出异常即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常。
try...catch
语句块的catch
可以有多个,但至少要有一个
try...catch
语句的执行过程是:
- 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
- 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕 获”),执行完后再跳转到后一个 catch 块后面继续执行。
void test()
{
double x, y;
cin >> x >> y;
try
{
if(0==y) //把数字放到前面能够判断失误(y=0这种情况)
{
throw y;
}
else
{
cout << "(x/y) = " << x/y <<endl;
}
}
catch(double d)
{
cout << "catch(double)" << endl;
}
catch(int i)
{
cout << "catch(int)" << endl;
}
}
十一、字符串
1.C
风格字符串
C
风格字符串是以'\0'
(空字符)来结尾的字符数组。对字符串进行操作的C函数定义在头文件<string.h>
或<cstring.h>
中
void test0()
{
char str[] = "hello";
char * pstr = "world";
//求取字符串长度
printf("%d\n", strlen(str));
//字符串拼接
char * ptmp = (char*)malloc(strlen(str) + strlen(pstr) + 1);
strcpy(ptmp, str);
strcat(ptmp, pstr);
printf("%s\n", ptmp);
//查找子串
char * p1 = strstr(ptmp, "world");
free(ptmp);
}
2.C++
风格字符串
C++
提供了std::string
(后面简写为string)类用于字符串的处理。string
类定义在C++
头文件<string>
中,注意和头文件<cstring>
区分,<cstring>
其实 是对C
标准库中的<string.h>
的封装,其定义的是一些对C
风格字符串的处理函数。
void test1()
{
//C风格字符串转换为C++风格字符串
std::string s1 = "hello";
std::string s2("world");
//C++风格字符串转换为C风格字符串
const char *pstr=s1.c_str();
cout << "pstr = " << pstr << endl;
//求取字符串长度
size_t len1 = s1.size();
size_t len2 = s1.length();
cout << "s1.size() = " << len1 << endl;
cout << "s1.length() = " << len2 << endl;
//字符串的遍历
for(size_t idx = 0; idx != s1.size(); ++idx)
{
cout << s1[idx] << " ";
}
cout << endl;
//字符串拼接
//C++拼接字符串的时候,不用考虑空间的申请与释放
string s3 = s1 + s2;
cout << "s3 = " << s3 << endl; //helloworld
string s4 = s1 + "wuhan";
cout << "s4 = " << s4 << endl; //hellowuhan
s4.append(s2);
cout << "s4 = " << s4 << endl; //hellowuhanworld
//C++风格字符串的查找
size_t pos =s4.find("wuhan");
cout << "pos = " << pos << endl; //pos=5
//截取子串
string substr = s4.substr(pos,5); //pos位置后截取5个
cout << "substr = " << substr << endl; //wuhan
string substr = s4.substr(pos); //pos位置后截取全部
cout << "substr = " << substr << endl; //wuhanworld
}
十二、程序内存分布
32bit系统 232=4G
3G - 4G 内核空间
0G - 3G 用户态空间
栈区(stack):编译器进行自动分配与释放 函数参数 局部变量
堆区(heap):程序员进行控制 malloc new
读写段
全局/静态区:全局静态变量
只读段:
文字常量区:文字常量"helloworld"
程序代码区:程序的二进制代码
内存对齐
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
解释二
原因有这么几点:
1、CPU 对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16 … 字节;
2、当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣;
tr << endl; //wuhan
string substr = s4.substr(pos); //pos位置后截取全部
cout << "substr = " << substr << endl; //wuhanworld
}
## 十二、程序内存分布
32bit系统 2^32^=4G
3G - 4G 内核空间
0G - 3G 用户态空间
栈区(stack):编译器进行自动分配与释放 函数参数 局部变量
堆区(heap):程序员进行控制 malloc new
读写段
全局/静态区:全局静态变量
只读段:
文字常量区:文字常量"helloworld"
程序代码区:程序的二进制代码
[外链图片转存中...(img-Whs7fWsr-1594652400385)]
### 内存对齐
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
解释二
原因有这么几点:
1、CPU 对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16 ... 字节;
2、当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣;
3、某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则会产生异常。