第一章 认识C++
1.1 命名空间
1.1.1 命名空间的基本格式
- 命名空间是一个由用户自己定义的作用域,在不同作用域中定义相同变量,不会冲突。
- 命名空间中可以存放以下类型,这些定义/声明在结构体中的内容成为实体
- 变量
- 常量
- 函数(可以是定义或声明)
- 结构体
- 类
- 模板
- 命名空间(可以嵌套定义)
namespace wd{
int number = 0;
struct Foo
{
int val;
}
void display();
}//end of namespace wd
1.1.2 命名空间的使用方式
-
命名空间一共有三种使用方式
-
using编译指令
-
将该空间中的全部实体一次性引入到程序中
using namespace std;
-
-
作用域限定符
-
每次要使用某个名称空间中的实体时,都直接加上"::"
namespace wd { int number = 10; void display() { //cout,endl都是std空间中的实体,所以都加上'std::'命名空间 std::cout << "wd::display()" << std::endl; } }
-
-
using声明机制
- using声明机制的作用域是从using语句开始,到using所在的作用域结束。
- 在同一作用域内用using声明的不同的命名空间的成员不能有同名的成员,否则会发生重定义。(同一作用域内不可重名)
//同一作用域内不同空间不可重名 #include <iostream> //using声明机制 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(); }
-
1.1.3 匿名命名空间
- 匿名空间可以不定义名字,该空间中的实体,其他文件无法引用,它只能在本文件的作用域内有效。
- 在匿名空间中创建的全局变量,具有全局生存周期,却只能被本空间内的函数访问,是static变量的有效替代手段。
namespace{
//其中vall只可以被本命名空间内的函数访问,是全局变量。
//外部空间不可以访问
int vall = 10;
void func();
}
1.1.4 命名空间的嵌套及覆盖
#include <iostream>
using namespace std;
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(number);
return 0;
}
- 只有在使用命名空间的时候,参数才是命名空间内部得参数
- 嵌套命名空间得时候,想使用嵌套命名空间中得参数,要嵌套使用::
1.1.5 命名空间的使用方法
-
在已命名的空间中定义变量,而不是直接定义外部全局变量或者静态变量
-
如果开发了一个函数库或者类库,提前将其放在一个名称空间中
-
对于using声明,将其作用域设置为局部而不是全局
-
不要在头文件中使用using编译指令,这样使得可用名称变得模糊,容易出现二义性。
-
包含头文件的顺序可能会影响程序的行为,如果非要使用using编译指令,建议放在所有#include预编译指令后。
比如
#include <iostream> using namespace std; //std命名空间就是头文件iostream中函数的声明位置
1.2 const关键字的用法
-
const 与宏定义的区别
- 编译器处理方式不同
- 宏定义是在预处理阶段展开,做字符串的替换
- const是常量在编译时候分配空间
- 类型和安全检查不同
- 宏定义没有类型,不做任何类型检查
- const常量具有具体的类型,在编译器会执行类型检查
- const常量必须要进行初始化
- 编译器处理方式不同
-
常量指针与指针常量
-
const pointer
指针可以改变指向,不可以通过指针修改常量的值const int *p1 = # int const *p1 = # //const直接修饰*p1表示*p1不可变,即变量的值不可变
-
pointer to const
指针不可以改变指向,可以通过指针改变变量
int* const p3 = &number; //const直接修饰p3,表示p3的值不可以改变,即指针的地址不可以改变
-
1.3 new/delete表达式
1.3.1 new/delete的作用
- new/delete在c++中用来开辟和回收堆空间
- malloc/free在c语言中用来开辟和回收堆空间
虚拟内存空间分布
低 高
代码段 存储代码指令 | 数据段 存储全局静态变量 | 堆空间 动态内存管理,由低->高生长 | 栈空间 存储局部变量,由低<-高生长 | 内核区域用户态->内核态 |
---|
1.3.2 用new/delete开辟空间
//开辟一个元素空间
int * p = new int(1);
//释放一个元素的空间
delete p;
//开辟一个数组空间
int * p = new int[10]();
//释放一个数组的空间
delete []p;
- new/delete自动初始化,自动分配空间大小;malloc不可以
1.4 引用
1.4.1 引用的概念
-
变量名是一段连续内存空间的别名,引用就是把这段连续的内存空间再取一个别名
void test0{ int a = 1; int & ref1 = a; //ref2是一个不完整的引用声明,它没有指向任何变量 //一个完整的引用一定要进行初始化不然会报错 int & ref2; }
-
引用要注意的点
- &不再是取地址符号,而是引用符号。
- 引用类型需要和被引用的值的类型保持一致
- 声明引用时候一定要初始化
- 一旦绑定到某个变量之后,就不会再改变其指向
1.4.2 引用的本质
引用就是被限制的指针,占据一个指针的内存,存放一个地址,一旦被绑定就不可以再改变
1.4.3 引用作为函数参数
- 通过形参改变改变实参的值
- 指针:不好操作,比较复杂
- 引用:有更好的可读性和实际意义
- 值传递:副本进行传递,不划算
1.4.4 引用作为函数的返回值
当以引用作为函数的返回值的时候,返回的变量其生命周期一定大于函数的生命周期,函数执行完毕时,返回的变量还存在。
int temp;
int & func1(){
temp = 100;
return temp;
}
-
返回的类型为引用要注意的点
-
不能返回局部变量的引用
-
不能在函数内部返回new分配的堆空间变量的引用。
如果返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量该引用所在的空间就无法释放,会造成内存泄漏。
-
//number是一个局部变量,func3结束之后就被销毁了,返回错误
int & func3(){
int number = 1;
return number;
}
//在函数内部申请的地址,在函数结束之后就释放了,但是却返回了引用(地址),导致内存泄漏
int & fun4(){
int * pint = new int(1);
return *pint;
}
- 引用总结
- 引用主要是用于参数传递时,解决副本,指针操作可读性差的问题
- 通过对const的使用,保证了引用传递的安全性。
第二章 类与对象基础
2.1 面向对象的思想
面向对象的三大基本特征是:
- 封装:隐藏内部实现
- 继承:复用现有代码
- 多态:改写对象行为
2.2 类的定义
2.2.1 概念速览
类的定义:
- 数据成员: 相当于现实世界中的属性
- 成员函数: 对数据的操作
- 注意:
- 定义类名遵循大驼峰规则
- 定义成员函数名遵循小驼峰规则
- 定义数据线成员名在前面加上下划线
class MyClass{//类的定义
//……
void myFunc(){} //成员函数
int _a; //数据成员
};//一定要有分号
//类也可以先声明,后完成定义
class MyClass2;//类的声明
class MyClass2{//类的定义
//……
};//分号不能省略
访问修饰符:
- public: 公有的访问权限,在类外可以通过对象直接访问公有成员。
- protected: 保护的访问权限,派生类中可以访问,在类外不能通过对象直接访问。
- private: 私有的访问权限,在本类之外不能访问,比较敏感的数据设为private。
- 注意:
- class定义中如果在成员定义(或声明)之前没有任何访问修饰符,其默认的访问权限为私有。
struct和class的对比:
C语言中的struct可以封装数据,但是不能隐藏数据,而且成员不能是函数。
C++中中的struct对C中的struct做了拓展,基本等同于class,默认访问权限是public。
class的默认访问权限是private。
typedef struct{
int number;
char name[25];
int score;
void (*p)();
//void print();//error
}
成员函数的定义:
成员函数定义的形式: 成员函数可以在类内部完成定义,也可以在类内部只进行声明,在类外部完成定义。
多文件联合编译时可能出现的错误: 如果在头文件中对函数进行定义,头文件内容在每个源文件都会复制一份,每个源文件都会生成一份目标文件,可能会导致在链接阶段出现相同函数定义的情况,导致重定义错误。
class Computer {
public:
//成员函数
void setBrand(const char * brand);//设置品牌
void setPrice(float price);//设置价格
void print();//打印信息
private:
//数据成员
char _brand[20];
float _price;
};
void Computer::setBrand(const char * brand){
strcpy(_brand, brand);
}
void Computer::setPrice(float price){
_price = price;
}
解决多文件联合编译错误的方法:
-
解决方法1:在成员函数的定义前加上inline关键字,说明类内部定义的成员函数就是inline函数
inline void Computer::setBrand(const char * brand){ strcpy(_brand, brand); } inline void Computer::setPrice(float price){ _price = price; }
-
解决方法2:将成员函数的定义放在类的内部(和方法一本质上是一样的效果)
-
解决方法3:函数声明放在头文件,函数定义放在实现文件中(一个.c一个.h)。
最常用的方法就是方法3
构造函数(如何创建一个对象):
构造函数: 和类同名的函数称为构造函数,构造函数可以重载,如果没有构造函数自动用无参构造函数。
初始化列表: 利用初始化列表对对象的数据成员完成初始化,数据成员初始化的顺序与其声明的顺序保持一致,与他们在初始化列表中的顺序无关(但初始化列表一般保持与数据成员声明的顺序一致)
class Point {
public:
Point(){}
Point(int ix,int iy = 0);
//可以在声明中设定参数的默认值
private:
//c++11之后也可以初始化数据成员,但是一般情况下还是在初始化列表中对数据成员初始化
int _ix;
int _iy;
};
inline void Point::Point(int ix, int iy)
:_ix(ix)
,_iy(iy)
{
cout << "Point(int,int)" << endl;
}
指针数据成员初始化:
类的数据成员中有指针时,意味着创建该类的对象时要进行指针成员的初始化,需要申请堆空间。
class Computer {
public:
Computer(const char * brand, double price)
//+1加的是\0
: _brand(new char[strlen(brand) + 1]())//这里新new的内存要交给析构函数进行回收
, _price(price)
{
strcpy(_brand,brand);
}
private:
char * _brand;
double _price;
};
void test0(){
Computer pc("Apple",12000);
}
对象所占空间大小:内存对齐机制,为什么要进行内存对齐:
- 1.平台原因: 不是所有的而硬件平台都能访问任意地址上的任意数据的。
- 2.性能原因: 64位系统默认以8个字节块大小进行读取,对齐可以防止系统读取一块儿数据的时候多次访问内存。
- 规则:按照类中占空间最大的数据成员大小的倍数对齐。
- 注意:如果数据成员中有数组类型,会按照除数组以外的其他数据成员中最大的那一个的倍数 对齐。
class C{
int _c1;
int _c2;
double _c3;
};
//sizeof(C) = 16
class D{
int _d1;
double _d2;
int _d3;
};
//sizeof(D) = 24
析构函数(如何销毁一个对象):
析构函数的概念:
- 定义:对象在销毁时调用的函数
- 作用:清理对象的数据成员申请的资源(堆空间)
- 形式:
- 没有返回值,即使是void也没有。
- 没有参数。
- 函数名与类名相同,在类名之前要加上一个(不加就是构造函数)
- 性质:
- 析构函数不可以重载(构造函数可以)
- 析构函数默认情况下,系统会自动提供一个
- 当对象被销毁时,会自动调用析构函数
- 不建议手动调用析构函数,因为容易导致各种问题,应该让析构函数自动被调用。
自定义析构函数:
-
什么时候需要自定义析构函数: 当数据成员中有指针时,创建一个对象,会申请堆空间,销毁对象时默认析构不够用了 (造成内存泄漏),此时就需要我们自定义析构函数。在析构函数中定义堆空间上内存回 收的机制,就不会发生内存泄漏。
class Computer { public: Computer(const char * brand, double price) : _brand(new char[strlen(brand) + 1]()) , _price(price) {} ~Computer() { if(_brand){ delete [] _brand; _brand = nullptr//设为空指针,安全回收 } cout << "~Computer()" << endl; } private: char * _brand; double _price; };
构造函数和析构函数的调用时机(重点):
-
全局定义的对象: 在主函数main接收程序控制权之前,就调用构造函数创建全局对象,在整个程序结束时,自动调用全局对象的析构函数。
-
局部定义的对象: 每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
-
static定义的静态对象: 当程序流程到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
-
new创建的堆对象: 每当创建该对象时调用构造函数,在使用delete删除该对象时,调用析构函数。
Computer *p1 = new Computer("Lenovo",6500); P1->print(); delete P1; P1 = nullptr;
拷贝构造函数(如何复制一个对象):
拷贝构造函数的定义: 用一个变量初始化另一个变量
拷贝构造函数的形式: 类名(const 类名 &)
- 拷贝构造函数也是一个构造函数
- 该函数用一个已经存在的同类型的对象,来初始化新的对象,即对对象本身进行复制
拷贝构造函数的形式探究:
-
拷贝构造函数是否可以去掉引用符号:
不可以。
如果拷贝函数的参数中去掉引用符号,进行拷贝时调用拷贝构造函数的过程中会发生“实参和形参都是对象,用实参初始化形参”(拷贝构造第二种调用时机),会再一次调用拷贝构 造函数。形成递归调用,直到栈溢出,导致程序崩溃。
-
拷贝构造函数是否可以去掉const:
在复制临时对象内容的时候会报错:
Computer pc3 = Computer("ASUS",5000);
- 加const的第一个用意: 为了确保右操作数的数据成员不被改变。
- 加const的第二个用意: 为了能够复制临时对象的内容,因为非const引用不能绑定临时变 量(右值)。
拷贝构造函数的性质:
-
使用默认的拷贝构造函数,会进行浅拷贝,即两个指针指向同一片内存
Point(const Point & rhs) : _ix(rhs._ix) , _iy(rhs._iy) {}
-
当要进行拷贝构造的类的数据成员有指针(要申请堆空间)的时候,要将拷贝构造显示写出,采用深拷贝的方式,先申请空间,再复制内容。
Computer::Computer(const Computer & rhs) : _brand(new char[strlen(rhs._brand) + 1]()) , _price(rhs._price) { strcpy(_brand, rhs._brand); }
拷贝构造函数的调用时机(重点):
-
当使用一个已经存在的对象初始化另一个同类型的新对象时。
-
当函数参数(实参和形参的类型都是对象),形参与实参结合时(实参初始化形参)。
为了避免这次不必要的拷贝,可以使用引用作为参数。
//调用拷贝构造 void func(Comptuer rhs){ rhs.print(); } //引用避免调用拷贝构造 void func(Computer & rhs){ rhs.print(); } void test1(){ Computer pc("apple",2000); func(pc); }
-
当函数的返回值是对象,执行return语句时(编译器有优化)。
为了避免这次多余的拷贝,可以使用引用作为返回值,但一定要确保返回值的生命周期大于函数的生命周期。
Computer pc3("Acer",5400); //调用拷贝构造函数 Computer func2(){ return pc3; } //直接引用,避免调用拷贝构造函数 Computer & func2(){ return pc3; }
赋值运算符函数:
赋值运算符函数的执行时机: 在执行 pt1 = pt2; 该语句时, pt1 与 pt2 都存在,所以不存在对象的构造,这要与 Point pt2 =pt1; 语句区分开,这是不同的。
Point pt1(1, 2), pt2(3, 4);
pt1 = pt2;//赋值操作
赋值运算符函数的形式: 类名& operator=(const 类名 &)
Point & Point::operator=(const Point & rhs)
{
_ix = rhs._ix;
_iy = rhs._iy;
//返回本对象的this指针
return *this;
}
赋值运算符函数的形式探究:
-
赋值运算符函数的返回必须是一个引用吗?
可以不是但是会造成一次多余的拷贝,增加不必要的开销。
-
赋值操作符函数的返回类型可以是void吗?
可以是但是无法处理连续赋值
-
赋值操作符函数的参数一定要是引用吗?
可以不是但是会造成一次多余的拷贝,增加不必要的开销
-
赋值操作符函数的参数必须是一个const引用吗?
无法避免在赋值运算符函数中修改右操作的内容,不合里,而且当右操作数为临时对象的时候,不是const的引用会报错。
赋值运算符函数的定义:
-
如果对象的指针数据成员申请了堆空间,默认的赋值运算符函数就不够用了,是浅拷贝。
Computer & operator=(const Computer & rhs){ this->_brand = rhs._brand; this->_price = rhs._price; return *this; }
-
直接进行深拷贝是否可行?
不可行,会发生内存泄漏。
因为创建对象的时候就已经申请了空间,只有先把创建对象时候申请的空间释放,然后再申请空间才不会造成内存泄漏。
Computer & operator=(const Computer & rhs){ if(this != &rhs){ delete [] _brand; _brand = new char[strlen(rhs._brand)](); strcpy(_brand,rhs._brand); _price = rhs._price; } return *this; }
-
总结——四步走(重点):
- 考虑自复制问题
- 回收左操作数原本申请的堆空间
- 深拷贝(以及其他的数据成员的复制)
- 返回*this
this指针:
this指针的本质:this指针指向本对象,this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
this指针存在那里: 寄存器——编译器在生成程序时加入了获取对象首地址的相关代码,将获取的首地址存放在了寄存器 中,这就是this指针。
this指针的生命周期: 开始于成员函数的执行开始,结束于成员 函数的执行结束。如果成员函数是通过一个已经销毁或未初始化的对象调用的,this指针将是悬挂的,它的 使用将会是未定义行为。
Point & operator=(const Point & rhs){
this->_ix = rhs._ix;
this->_iy = rhs._iy;
cout << "Point & operator=(const Point &)" << endl;
return *this;
}
成员函数中可以加上this指针,展示本对象通过this指针找到本对象的数据成员。但是不要 在参数列表中显式加上this指针,因为编译器一定会在参数列表的第一位加上this指针,如果显式再给一个,参数数量就不对了。
三合成原则
拷贝构造函数、赋值运算符函数、析构函数,如果需要手动定义其中的一个,那么另外两 个也需要手动定义。
标签:const,函数,int,brand,笔记,学习,Computer,C++,构造函数 From: https://www.cnblogs.com/aCuteRabbit/p/18066611