C++基础(一)
前言
个人学习笔记
一、命名空间
为了解决在同一作用域中有名字冲突,C++引入了命名空间,通过作用域限定符,解决名字相同实体(定义在名称空间中的变量或者函数)之间的冲突,其中实体的可见域是从实体创建到该名称空间结束。在名称空间外,该实体是不可见的。命名空间中可以定义变量、常量、函数(可以是声明或定义)、结构体、类、命名空间等, C++中命名空间是通过using编译指令,若对某命名空间不熟悉,可能会定义与之相同名字的实体,造成冲突,所以建议通过using声明机制。
//C++的头文件都是用模板进行编写,所以不加.h
//而模板有一个特点就是必须知道所有实现之后才能正常编译
#include <iostream>
//using namespace std;//命名空间,using编译指令,
//它的作用就是把命名空间中的实体一次性全部引出来,
//可能导致自己定义的实体与std里面的实体冲突
//#if 0
//using声明机制
using std::cout;//一次只将命名空间中的一个实体引出来,需要哪个引出哪个,
//并且不会定义相同名字的实体产生二义性
using std::endl;
using std::string;
//#endif
namespace exampleA//带命名空间的函数声明
{
void display();
}
//命名空间可以扩展(自定义命名空间可以扩展;标准命名空间也可以扩展)
namespace exampleA
{
int var = 10;
const char a[] = "hello,world";
struct Mystruct
{
int number = 100;
void test();//函数声明
};
//函数的申明是可以有多次,而函数的定义只能有一次
void display()//函数定义
{
cout << "exampleA::display()" << endl;
}
namespace exampleAA
{
int var1 = 1;
void display()
{
cout << "exampleA::exampleAA::display()" << endl;
}
}
class student
{
public:
student()
:_name(nullptr)
,_age(0)
{
cout << "student()" << endl;
}
~student()
{
cout << "~student()" << endl;
}
void add() const
{
/*_age = 1;*/ //error
cout << "age = " << _age <<endl;
}
private:
string _name;
int _age;
};
}
int main(int argv, char *args[])
{
exampleA::display();//::作用域限定符,即使自定义的实体与std命名空间中的实体冲突,也没有问题
exampleA::exampleAA::display();
return 0;
}
二、const关键字(常量不可以赋值)
C语言中定义常量通过define,如#define MAX 10,宏定义发生的时机在预处理阶段,如果有bug只能到运行的时候才能发现;C++定义常量通过const关键字,const可以修饰变量,变量就称为一个常量,常量在定义的时候必须要进行初始化,const可以在类型前也可以在类型后,如const int number = 10 与int const number = 10二者等价,其次const发生时机在编译阶段,如果有bug,就会立即报出来,相比宏定义要安全一些。
2.1 const修饰变量与指针
#include <iostream>
using namespace std;
void test()
{
const int a = 10;
int var = 20;
const int *p = &var;//常量指针
*p = 1;//错误,通过p指针无法修改其所指内容的值
p = nullptr;//正确,可以改变p指针的指向
int const *p1 = &var;//常量指针
*p1 = 2;//错误,通过p1指针无法修改其所指内容的值
p1 = nullptr;//正确,可以改变p1指针的指向
int * const p3 = &var;//指针常量
*p3 = 3;//正确,可以通过p3指针修改其所指内容的值
p3 = nullptr;//错误,不可以改变p3指针的指向
const int * const p4 = &var;//双const,两者皆不能修改
*p4 = 4;//错误,不能通过p4指针修改其所指内容的值
p4 = nullptr;//错误,不可以改p4指针的指向
}
int main()
{
std::cout << "Hello world" << std::endl;
return 0;
}
2.2 const修饰成员函数与对象
const修饰成员函数时一般放在函数最后,如void add() const,对于不改变数据成员的成员函数都要在后面加const,const关键字对成员函数的行为作了更加明确的限定:有const 修饰的成员函数,只能读取成员变量,不能改变成员变量;没有const修饰的成员函数,则可读可写。
const修饰对象时一般放最前面,如const student std(student为一个类),有以下几个特点:
- const修饰对象,该对象为常对象,不能修改对象的值。
- 可以调用const成员函数,不可以调用非const成员函数。
由指针常量与常量指针,又可以联想到以下概念的区别(以int型为例)。
名称 | 形式 | 含义 |
---|---|---|
常量指针 | const int * p | 不可以通过*p修改指针指向地址的内容,可以改变指针的指向 |
指针常量 | int * const p | 可以通过*p修改指针指向地址的内容,不可以改变指针的指向 |
函数指针 | int (*pfunc)(int) | 指向返回类型为int,参数为int型函数的指针 |
指针函数 | int* pfunc(int) | 一个返回类型是int*的函数 |
数组指针 | int (*pArray)[] | 指向一个int型数组 |
指针数组 | int* pArray[] | 一个名为pArray的数组,里面存放的是int*指针 |
三、malloc/free与new/delete区别
相同点:
- 都是用来申请堆空间;
- 必须成对出现,不成对出现会造成内存泄漏。
不同点:
- malloc/free是c语言的库函数,new/delete是C++的关键字(运算符或表达式);
- malloc只能申请原始的堆空间,new申请堆空间并进行初始化
- new/delete能对对象进行构造和析构函数的调用,进而对内存进行更加详细的工作,而 malloc /free不能。
#include <iostream>
#include <stdlib.h>// malloc/free
#include <string.h>// memset
#include <stdio.h>// printf
using namespace std;
//malloc/free与new/delete的区别
//相同点:1、都是用来申请堆空间;2、必须成对出现,不成对出现会造成内存泄漏
//不同点:1、malloc/free是c语言的库函数,new/delete是C++的关键字(运算符或表达式)
// 2、malloc之恩那个申请原始的堆空间,new申请堆空间并进行初始化
void test1()
{
int *pInt = (int *)malloc(sizeof(int));//1、申请堆空间
memset(pInt, 0, sizeof(int));//2、初始化(清零)
*pInt = 10;//3、赋值
printf("*p = %d\n", *pInt);
printf("&p = %p\n", &pInt);
printf("p = %p\n", pInt);
free(pInt);//4、回收堆空间
}
void test2()
{
int *pInt = new int(10);//1、申请堆空间,并进行初始化,最后赋值10
cout << "pInt = " << *pInt << endl;
delete pInt;//2、回收堆空间
cout << endl;
int *pArray = new int[10]();//1、申请堆空间,并进行初始化
for(size_t idx = 0; idx < 10; ++idx)
{
pArray[idx] = idx;
}
delete [] pArray;//2、释放堆空间[]
}
int main()
{
test1();
test2();
return 0;
}
由内存泄漏又可以联想到以下概念:
名称 | 含义 |
---|---|
内存泄漏 | 程序在申请内存后,无法释放已申请的内存空间(最终会导致内存溢出) |
内存溢出 | 可用的内存均被占用,无法申请内存的情况 |
内存踩踏 | 访问了不属于自己的地址 |
空悬指针 | 指的是一个指针,当它指向的对象已经被释放的时候而自身却没有被置为null的时候 |
野指针 | 没有进行初始化的指针,一个指针没有初始化的时候会一通乱指,这个时候就类似于空悬指针 |
四、引用
变量其实质是一段连续内存空间的别名,而引用是变量的别名,或者可以说一段连续内存空间的引用是变量,需要注意的是引用必须初始化,且一经绑定后,就不可以改变其指向,C++中的引用本质上是一种被限制的指针,是一个指针常量,所以引用是占据内存空间的,大小就是一个指针的大小,引用的提出是为了减少指针的使用。
单纯的给某变量取别名是没有任何意义,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
在64位系统中,一个int型指针占8个字节,通过指针可以间接对变量操作,但需要对指针变量分配内存空间,相比引用效率低且可读性较差;而引用就是变量别名,对引用操作就是对变量本身进行操作,所以效率较高。引用一般有2种用法:一是引用作为函数参数;二是引用作为函数的返回值。
4.1 引用作为函数参数
#include <iostream>
using namespace std;
void swap(int x, int y)//值传递
//系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量,
//即形参变量只是实参变量的副本而已,即int x = a,int y = b
{
int tmp = x;
x = y;
y = tmp;
}
void swap1(int *x, int *y)//地址传递,即int *x = &a, int *y = &b
//需要为形参指针变量在内存中分配空间,通过指针变量间接对变量本身进行操作
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void swap2(int &x, int &y)//即int &x = a, int &y = b
//与操作变量本身是一样的,引用就是变量别名
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 3,b = 4;
cout << "a =" << a << ",b = " << b << endl;
swap(a, b);
cout << "a =" << a << ",b = " << b << endl;
swap1(&a, &b);
cout << "a =" << a << ",b = " << b << endl;
swap2(a, b);
cout << "a =" << a << ",b = " << b << endl;
return 0;
}
4.2 引用作为函数返回值
当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在,也就是说引用作为函数的返回值时,不可以返回局部变量的引用,因为局部变量会在函数返回后被销毁,因此被返回的引用就成为 了"无所指"的引用,程序会进入未知状态。不要返回堆空间引用,因为堆空间引用所在的空间无法释放,会造成内存泄漏,除非有内存回收的机制。
#include <iostream>
using namespace std;
int arr[10] = {1,2,3,4};
int &getdata()
{
return arr[0];
}
void test2()
{
cout << "getdata() = " << getdata() << endl;
getdata() = 100;
cout << "getdata() = " << getdata() << endl;
cout << "arr[0] = " << arr[0] << endl;
}
//不要返回局部变量的引用
int &func()
{
int var = 100;
return var;//生命周期已经结束
}
//函数返回引用的前提:实体的生命周期一定要大于函数的生命周期
//建议不要返回堆空间的引用,除非有内存回收的机制
int &getHeapdata()
{
int *number = new int(10);//堆空间的生命周期会一直持续,直到释放
return *number;
}
void test3()
{
int a = 3, b = 4;
int c = a + getHeapdata() + b;//有内存泄漏的隐患
cout << "c = " << c << endl;
int &ref = getHeapdata();//用引用接收
delete &ref;//删除引用所指向的堆空间
}
int main()
{
test2();
test3();
return 0;
}
五、C++强制转换
传统C语言的强制转换有很多缺点,因为它可以在任意类型之间进行转换,比如可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成一个指向派生类对象的指针,显然这二者差别很大,还有就是c风格的转换不容易查找,它由一个括号加上一个标识符组成,而这样的代码在C++程序到处都是。为了克服这些缺点,C++引进了4个新的类型转换操作符,分别是:
- static_cast
- const_cast
- dynamic_cast
- reinterpret_cast
#include <iostream>
using namespace std;
void test()
{
int iNumber = 10;
float fNumber = 12.34;
iNumber = (int)fNumber;//c语言强制转换
iNumber = int(fNumber);
iNumber = static_cast<int>(fNumber);
cout << "iNumber = " << iNumber << endl;
void *pret = malloc(sizeof(int));
int *pInt = static_cast<int *>(pret);//void *型指针转换为int *型指针
delete pInt;
pInt = nullptr;//指针置为空
}
void test2()
{
const int number = 100;
/* int *p = &number;//error */
int *p = const_cast<int *>(&number);
cout << "*p = " << *p << endl;
cout << endl;
*p = 200;//c++中未定义的行为
cout << "number = " << number << endl;
cout << "*p = " << *p << endl;
printf("&number = %p\n", &number);
printf("&p = %p\n", p);
}
int main()
{
test();
test2();
return 0;
}
5.1 static_cast用法
1)用于基本数据类型之间的转换,如把int转换成char,把int转换成enum 。
2)把void *指针转换成目标类型的指针,但不安全。
3)把任何类型的表达式转换成void类型。
4)用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。
5.2 const_cast用法
修改变量或对象的const属性,把常量属性移除。
5.3 dynamic_cast用法
主要用于基类和派生类间的转换,尤其是向下转型的用法中。
5.4 reinterpret_cast用法
用来处理无关类型之间的转换,任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。错误的使用reinterpret_cast容易导致程序不安全,只有将转换后的类型值转换回到其原始类型,才是正确使用reinterpret_cast的方式。