c++难点总结


一、基础入门

注意:我们在定义变量、结构体时(如 int a=10),其实我们在代码区保存了a的地址(&a),当代码运行到的时候,就会找到a的值。
而我们在定义指针、数组时(如 int*p=&a),我们在代码区保存了p的地址,而在p的内容则是a的地址,当代码运行到的时候,其实将通过代码区p的地址找到p的值(就是&a)。

总结一下就是代码区保存各种变量的地址,无论是普通变量、结构体、数组还是指针。但变量和结构体在内存保存的是值不是地址,而数组和指针则是保存的值是32位的地址。

int a=10;代码区保存了&a
int*p=& a;代码区保存了&p

os在运行代码区的代码时。通过&a、&p找到全局变量区的a和p,而p的内容则是a的地址&a,我们可以通过*p表示a的内容。

1.基础知识

对于未初始化的全局变量一般初始化为0;但对于未初始化的指针则是随意赋值的;对于new出来的未初始化的堆区也是0,但对于malloc出来的堆区则是随意赋值的。
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
++前置递增 a=2; b=++a; 结果:a=3; b=3;
++后置递增 a=2; b=a++; 结果:a=3; b=2;
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
利用sizeof关键字可以统计数据和类型所占内存字节数
int a=10;
sizeof(a);
sizeof(int);
数据类型****               占用空间**                                 取值范围s
short(短整型)               2字节                                 (-2^15 ~ 2^15-1)
int(整型)                  4字节                                (-2^31 ~ 2^31-1)
long(长整形) Windows为4字节,Linux为4字节(32)8字节(64)      (-2^31 ~ 2^31-1)
long long(长长整形)        8字节                                 (-2^63 ~ 2^63-1)
float                     4字节                                   7位有效数字
double                    8字节                                    1516位有效数字
_____________________________________________________________________________________________________
do...while循环语句
与while的区别在于:do...while会先执行一次循环语句==,再判断循环条件
int main() {

	int num = 0;

	do
	{
		cout << num << endl;
		num++;

	} while (num < 10);
	
	
	system("pause");

	return 0;
}

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
 goto语句:无条件跳转语句
int main() {

	cout << "1" << endl;

	goto FLAG;

	cout << "2" << endl;
	cout << "3" << endl;
	cout << "4" << endl;

	FLAG:

	cout << "5" << endl;
	
	system("pause");

	return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————
&按位与
&&

2.数组和指针

数组和指针没有本质区别的:p和a都指向数组的首地址。但数组作为参数传入函数时,往往代表数组本身,而指针代表数组的首地址。
但指针在作为sizeof的参数时,返回的是数组的字节数,而指针则表示的是指针本身的长度。
int a[]={};
int*p=a;
cout<<sizeof(a)<<sizeof(p);
cout<<a<<p;
数组可以用下面的方法遍历,而指针不行
for (int i:a)
{cout<<i;}
_______________________________________________________________________________________________________________________
C++独有的数组遍历方式
int main(int argc, char **argv)
{
    int a[]={1,2,3,4,5,6,7,8,9};
    for (int i : a )
    cout<<i<<endl
    
    不能把数组地址传给指针,在由指针通过这种方式遍历。
    //  int*p=a; //指针必须通过数组得到数组。
    //for (int i : p )
   // cout<<i<<endl;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
二维数组实际上是一个一重指针不是双重指针,a[3][2],a指向数组首地址,但a【0】指向第一行的首地址,这是编译器设置的。我们用数值指针指向一个二维数组。
	int a[3][2] = { 1,2,3,4,4,5 };
	int(* p)[2] = a;
	二维数组只能用这种方式的指针接收二维数组。不能用*P和*PP接收一个二维数组。
我们分析一下int(* p)[2],(*P)作为(*P)[2]的一维数组标号自然指向这个一维数组的首地址,这个一维数组的每个元素都是
数组(a的前三个元素组成的数组和后三个元素组成的数组),而p又是指向的a前三个组成的数组,所以p就指向了a,也就是数组的首地址。
总结一下:我们的(*p)同时又是一个指针类型。当int(* p)[2]指向一个二维数组int a[3][2]*p就是a【0】,也就是第一行的首地址,p就是a,也就是整个数组的首地址。
p==a;
*p==a[0]**p==a[0][0];

对于我们的数组指针int*pp[3],其实是一个一维数组,数组里存放的是地址。所以我们可以用二维指针去指向
int**p=pp;

一维数组定义的三种方式:

1. 数据类型  数组名[ 数组长度 ]; 
2. 数据类型  数组名[ 数组长度 ] = {1,值2 ...};
3. 数据类型  数组名[ ] = {1,值2 ...}; 

二维数组定义的四种方式:

5. ` 数据类型  数组名[ 行数 ][ 列数 ]; `
6. `数据类型  数组名[ 行数 ][ 列数 ] = { {数据1,数据2 }{数据3,数据4 } };`
7. `数据类型  数组名[ 行数 ][ 列数 ] = { 数据1,数据2,数据3,数据4};`
8. ` 数据类型  数组名[  ][ 列数 ] = { 数据1,数据2,数据3,数据4};

int main() {

	//二维数组数组名
	int arr[2][3] =
	{
		{1,2,3},
		{4,5,6}
	};

	cout << "二维数组大小: " << sizeof(arr) << endl;
	cout << "二维数组一行大小: " << sizeof(arr[0]) << endl;
	cout << "二维数组元素大小: " << sizeof(arr[0][0]) << endl;

	cout << "二维数组行数: " << sizeof(arr) / sizeof(arr[0]) << endl;
	cout << "二维数组列数: " << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;

	//地址
	cout << "二维数组首地址:" << arr << endl;
	cout << "二维数组第一行地址:" << arr[0] << endl;
	cout << "二维数组第二行地址:" << arr[1] << endl;

	cout << "二维数组第一个元素地址:" << &arr[0][0] << endl;
	cout << "二维数组第二个元素地址:" << &arr[0][1] << endl;

	system("pause");

	return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
函数分文件编写一般有4个步骤

1. 创建后缀名为.h的头文件  
2. 创建后缀名为.cpp的源文件
3. 在头文件中写函数的声明
4. 在源文件中写函数的定义

//swap.h文件
#include<iostream>
using namespace std;
//实现两个数字交换的函数声明
void swap(int a, int b);


//swap.cpp文件
#include "swap.h"

void swap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

//main函数文件
#include "swap.h"
int main() {

	int a = 100;
	int b = 200;
	swap(a, b);

	system("pause");

	return 0;
}


-————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
int main() {

	int a = 10;

	int * p;
	p = &a; //指针指向数据a的地址

	cout << *p << endl; //* 解引用
	cout << sizeof(p) << endl;
	cout << sizeof(char *) << endl;
	cout << sizeof(float *) << endl;
	cout << sizeof(double *) << endl;

	system("pause");

	return 0;
}
所有指针类型在32位操作系统下是4个字节

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
指针常量和常量指针都是需要初始化的。
const修饰指针:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
int main() {

	int a = 10;
	int b = 10;

	//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
	const int * p1 = &a; 
	p1 = &b; //正确
	//*p1 = 100;  报错
	

	//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
	int * const p2 = &a;
	//p2 = &b; //错误
	*p2 = 100; //正确

    //const既修饰指针又修饰常量
	const int * const p3 = &a;
	//p3 = &b; //错误
	//*p3 = 100; //错误

	system("pause");

	return 0;
}


值传递和地址传递,函数调用时将实参中的值传给形参。当将地址作为参数传给形参时,形参也会指向实参指向的内存位置。
//值传递
void swap1(int a ,int b)
{
	int temp = a;
	a = b; 
	b = temp;
}
//地址传递
void swap2(int * p1, int *p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

int main() {

	int a = 10;
	int b = 20;
	swap1(a, b); // 值传递不会改变实参

	swap2(&a, &b); //地址传递会改变实参

	cout << "a = " << a << endl;

	cout << "b = " << b << endl;

	system("pause");

	return 0;
}

3.结构体

语法:struct 结构体名 { 结构体成员列表 };`

通过结构体创建变量的方式有三种:

- struct 结构体名 变量名
- struct 结构体名 变量名 = { 成员1值 , 成员2...}
- 定义结构体时顺便创建变量

//结构体定义
struct student
{
	//成员列表
	string name;  //姓名
	int age;      //年龄
	int score;    //分数
}stu3; //结构体变量创建方式3 


int main() {

	//结构体变量创建方式1
	struct student stu1; //struct 关键字可以省略

	stu1.name = "张三";
	stu1.age = 18;
	stu1.score = 100;
	
	cout << "姓名:" << stu1.name << " 年龄:" << stu1.age  << " 分数:" << stu1.score << endl;

	//结构体变量创建方式2
	struct student stu2 = { "李四",19,60 };

	cout << "姓名:" << stu2.name << " 年龄:" << stu2.age  << " 分数:" << stu2.score << endl;


	stu3.name = "王五";
	stu3.age = 18;
	stu3.score = 80;
	
//结构体数组
	struct student arr[3]=
	{
		{"张三",18,80 },
		{"李四",19,60 },
		{"王五",20,70 }
	};
	cout << "姓名:" << stu3.name << " 年龄:" << stu3.age  << " 分数:" << stu3.score << endl;

	system("pause");

	return 0;
}


——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
数组符号表示数组的首地址
通过改变指针的选择指向数组的某个值
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int * p = arr;  //指向数组的指针
* (p+1);
* 
结构体符号表示结构体本身
通过指定p的参数指定对应的值。
struct student stu = { "张三",18,100, };
struct student * p = &stu;
p->name ==*p.name;

二、核心编程

1.malloc/delete和new/free

我们若想在函数内定义一个数组a[n],数组的长度n又是一个变量(传参)时,我们不能定义一个普通的数组,因为即使在函数内数组的长度
也必须是常数,所以我们就得定义一个堆区的数组,堆区是动态增长的所以可以长度是可以是一个变量。int*a=new int[n];

void *malloc(int size)void free(void *FirstByte)
 
malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。
malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。
int* p;   
p = (int *) malloc (sizeof(int)*128);//分配128个(可根据实际需要替换该数值)整型存储单元,并将这128个连续的整型存储单元的首地址存储到指针变量p中  
free(p)
double *pd=(double *) malloc (sizeof(double)*12);//分配12个double型存储单元,并将首地址存储到指针变量pd中
free(pd)

new 返回指定类型的指针,并且可以自动计算所需要大小。
new 可以指定分配堆区的值,没有指定默认初始化0.
nt *p;   
p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);  初始化为0  
delete p ;// 释放单个对象

int *pi=new int(100); //指针pi所指向的对象初始化为100 
delete pi ;// 释放单个对象

int* parr;   
parr = new int [100]; //返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100;
delete [ ]parr;//释放数组 

new/delete和 malloc/free最大的不同在于:new/delete 对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。但malloc/free是库函数而不是运算符,
不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
c++对于非内部的数据类型,需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
对于内部数据而言,不需要调用构造和析构函数,因此new/delete malloc/free都是可以用的。但是对于非内部数据,需要使用new/delete

2.空指针和野指针

int*p=Null;
int*pi;
p初始化为Null,其实Null是指向本进程某个段的段首地址。
pi未初始化其值,系统会为其随意赋值,因此可能会指向一个一个不可以访问的内存区域。

造成野指针的几个情况:
	1.指针未初始化
	2.delete和free后,指向堆区的指针任然指向原来的地方,变成了野指针。
	3.全局指针指向了某个函数内部的变量,函数释放变量后,全局指针变成了野指针。

3.引用

引用常量指针必须初始化,引用内部实现其实就是一个int* const p;
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用```

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

值传递不会改变实参的值,而地址和引用传递会改变实参的值,这是因为引用将形参的指向实参,引用就像是变量的别名。
//1. 值传递
void mySwap01(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 10;
	int b = 20;

	mySwap01(a, b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap02(&a, &b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap03(a, b);
	cout << "a:" << a << " b:" << b << endl;

	system("pause");

	return 0;
}

_____________________________________________________________________________________________________________________________
引用局部变量,函数结束局部变量释放,引用的值也随之释放,因为引用本质上就是一个指针常量。
//返回局部变量引用
int& test01() {
	int a = 10; //局部变量
	return a;
}

//返回静态变量引用
int& test02() {
	static int a = 20;
	return a;
}

int main() {

	//不能返回局部变量的引用
	int& ref = test01();
	cout << "ref = " << ref << endl;
	cout << "ref = " << ref << endl;

	//如果函数做左值,那么必须返回引用
	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	test02() = 1000;

	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	system("pause");

	return 0;
}

4.函数进阶

占位符
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
	cout << "this is func" << endl;
}

int main() {

	func(10,10); //占位参数必须填补

	system("pause");

	return 0;
}



——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
函数重载时有无const的区别,当常数和const修饰的常变量作为实参传给新参时,有const重载的函数则调用有const重载的函数。
//1、引用作为重载条件

void func(int &a)
{
	cout << "func (int &a) 调用 " << endl;
}

void func(const int &a)
{
	cout << "func (const int &a) 调用 " << endl;
}


//2、函数重载碰到函数默认参数

void func2(int a, int b = 10)
{
	cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
	cout << "func2(int a) 调用" << endl;
}

int main() {
	
	int a = 10;
	func(a); //调用无const
	func(10);//调用有const


	//func2(10); //碰到默认参数产生歧义,需要避免

	system("pause");

	return 0;
}

5.类和对象

//三种权限
//公共权限  public     类内可以访问  类外可以访问
//保护权限  protected  类内可以访问  类外不可以访问
//私有权限  private    类内可以访问  类外不可以访问

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
structclass唯一区别在于默认权限不同
struct默认是公共
class默认是私有
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
编译器默认添加了一个无参构造函数和一个无参析构函数以及一个拷贝构造函数。
实例化常见方法:
Person p;

Person p1(10);
Person p4 = 10; // Person p4 = Person(10); 
Person p2 = Person(10); 

Person p3 = Person(p2);
Person p5 = p4; // Person p5 = Person(p4); 
Person p6(p4);

Person(10)、Person()、Person(p1)单独写就是匿名对象  当前行结束之后,马上析构
Person p()错误,编译器会以为是函数声明

//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	Person(const Person& p) {
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
	Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

	//2.1  括号法,常用
	Person p1(10);
	//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
	//Person p2();

	//2.2 显式法
	Person p2 = Person(10); 
	Person p3 = Person(p2);
	//Person(10)单独写就是匿名对象  当前行结束之后,马上析构

	//2.3 隐式转换法
	Person p4 = 10; // Person p4 = Person(10); 
	Person p5 = p4; // Person p5 = Person(p4); 

	//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
	//Person p5(p4);
}

int main() {

	test01();
	//test02();

	system("pause");

	return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
深拷贝和浅拷贝
我们在调用拷贝构造创建的新对象时,可能会将新对象里的某个指针指向旧对象指针指向的的同一个堆区,旧对象和新对象调用析构时,会报错。应该在拷贝构造的时候不能是,指针传指针,而是new一个新的堆区。
class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int age ,int height) {
		
		cout << "有参构造函数!" << endl;

		m_age = age;
		m_height = new int(height);
		
	}
	//拷贝构造函数  
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_age = p.m_age;
		m_height = new int(*p.m_height);
		
	}

	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
		if (m_height != NULL)
		{
			delete m_height;
		}
	}
public:
	int m_age;
	int* m_height;
};

void test01()
{
	Person p1(18, 180);

	Person p2(p1);

	cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;

	cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————
初始化列表
构造函数():属性1(1),属性2(值2... {}

class Person {
public:

	传统方式初始化
	//Person(int a, int b, int c) {
	//	m_A = a;
	//	m_B = b;
	//	m_C = c;
	//}

	//初始化列表方式初始化
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
	void PrintPerson() {
		cout << "mA:" << m_A << endl;
		cout << "mB:" << m_B << endl;
		cout << "mC:" << m_C << endl;
	}
private:
	int m_A;
	int m_B;
	int m_C;
};

int main() {

	Person p(1, 2, 3);
	p.PrintPerson();


	system("pause");

	return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
静态成员
- 静态成员变量
  - 所有对象共享同一份数据
  - 类内声明,类外初始化,并在初始化的时候分配空间(未初始化的静态成员变量不能使用)
 
- 静态成员函数
  - 所有对象共享同一个函数
  - 静态成员函数只能访问静态成员变量和静态成员函数
	静态成员函数没有this指针,保存在全局变量区,因而无法调用非静态成员。(非静态成员在实例化后,其地址各不相同,没有this指针就无法找到实例化的成员)
class Person
{

public:

	//静态成员函数特点:
	//1 程序共享一个函数
	//2 静态成员函数只能访问静态成员变量
	
	static void func()
	{
		cout << "func调用" << endl;
		m_A = 100;
		//m_B = 100; //错误,不可以访问非静态成员变量
	}

	static int m_A; //静态成员变量
	int m_B; // 
private:

	//静态成员函数也是有访问权限的
	static void func2()
	{
		cout << "func2调用" << endl;
	}
};
int Person::m_A = 10;


void test01()
{
	//静态成员变量两种访问方式

	//1、通过对象
	Person p1;
	p1.func();

	//2、通过类名
	Person::func();


	//Person::func2(); //私有权限访问不到
}

int main() {

	test01();

	system("pause");

	return 0;
}

——————————————————————————————————————————————————————————————————————————————————------------——————————————————————
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
每个实例化的对象都有各自的成员变量,但成员函数是共有的,对象里只能找到调用函数的地址。
空指针可以调用没有使用成员变量的函数的原因是,Person * p = NULL,指针虽然指向NULL,但在NULL此处生成了一个person地址结构的区域,该区域也保存了成员函数的地址,但其函数对象地址是固定的,成员函数是存在于对象之外的,对象只能保存了成员函数的地址。
//空指针访问成员函数
class Person {
public:

	void ShowClassName() {
		cout << "我是Person类!" << endl;
	}

	void ShowPerson() {
		if (this == NULL) {
			return;
		}
		cout << mAge << endl;
	}

public:
	int mAge;
};

void test01()
{
	Person * p = NULL;
	p->ShowClassName(); //空指针,可以调用成员函数
	p->ShowPerson();  //但是如果成员函数中用到了this指针,就不可以了
}

int main() {

	test01();

	system("pause");

	return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
const修饰函数和对象,要求不能更改未加关键字mutable变量的值和调用不是常成员函数的函数。

**常函数:**
- 成员函数后加const后我们称为这个函数为**常函数**
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
- 
**常对象:**

- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数


class Person {
public:
	Person() {
		m_A = 0;
		m_B = 0;
	}

	//this指针的本质是一个指针常量,指针的指向不可修改
	//如果想让指针指向的值也不可以修改,需要声明常函数
	void ShowPerson() const {
		//const Type* const pointer;
		//this = NULL; //不能修改指针的指向 Person* const this;
		//this->mA = 100; //但是this指针指向的对象的数据是可以修改的

		//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
		this->m_B = 100;
	}

	void MyFunc() const {
		//mA = 10000;
	}

public:
	int m_A;
	mutable int m_B; //可修改 可变的
};


//const修饰对象  常对象
void test01() {

	const Person person; //常量对象  
	cout << person.m_A << endl;
	//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
	person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

	//常对象访问成员函数
	person.MyFunc(); //常对象不能调用const的函数

}

int main() {

	test01();

	system("pause");

	return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
友元,全局函数,类,成员函数可以做友元,友元可以访问publicprivate成员。

//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building)

//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;

//告诉编译器  goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
运算符重载
class MyInteger {

	friend ostream& operator<<(ostream& out, MyInteger myint);

public:
	MyInteger() {
		m_Num = 0;
	}
	//前置++
	MyInteger& operator++() {
		//先++
		m_Num++;
		//再返回
		return *this;
	}

	//后置++
	MyInteger operator++(int) {
		//先返回
		MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
		m_Num++;
		return temp;
	}

private:
	int m_Num;
};


ostream& operator<<(ostream& out, MyInteger myint) {
	out << myint.m_Num;
	return out;
}


//前置++ 先++ 再返回
void test01() {
	MyInteger myInt;
	cout << ++myInt << endl;
	cout << myInt << endl;
}

//后置++ 先返回 再++
void test02() {

	MyInteger myInt;
	cout << myInt++ << endl;
	cout << myInt << endl;
}

int main() {

	test01();
	//test02();

	system("pause");

	return 0;
}

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
public继承可以访问publicprotected
protected继承可以访问protectedpublic
private继承可以访问privatepublic

子类无论那种方式继承父类,都继承了父类的所有成员变量,继承方式会将对应的某些成员变量隐藏起来。
子类不仅继承了父类的成员变量,也继承了父类的成员函数,和静态成员函数和静态成员变量。但是函数和今天成员是所有对象共有的,父子类中只是记录了其地址,所有实例化的对象都只是通过地址访问。

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
子类和父类有相同的成员函数名或成员变量名时,默认子类的对象调用子类本身的成员,而必须通过指定作用域来调用父类成员。
父子类都有变量m_A和函数fun();
s.func();
s.Base::func();

s.m_A
s.Base::m_A

父子类同名静态成员函数和静态成员变量时,除了非静态成员函数和变量的方法,还可以直接通过类名调用。
因为静态成员是共有的,非静态成员变量随着实例化而复制一份,每个成员变量都有其this指向的偏移地址,所以不能通过类名调用,
Son s;//实例化子类s
s.func();
s.Base::func();

Son::func();//通过类名调用
Son::Base::func();

s.m_A
s.Base::m_A
Son::m_A 
Son::Base::m_A

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
菱形继承
虚继承,当虚继承发生时,编译器会拷贝一份父类的数据,虚继承的类会通过指针维护这份数据,当孙类继承一个或多个虚继承的父类,孙类会继承父类维护的从爷类拷贝的数据。
class Animal
{
public:
	int m_Age;
};

//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo   : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 100;
	st.Tuo::m_Age = 200;

	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.Tuo::m_Age = " <<  st.Tuo::m_Age << endl;
	cout << "st.m_Age = " << st.m_Age << endl;
}


int main() {

	test01();

	system("pause");

	return 0;
}

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定  -  编译阶段确定函数地址
- 动态多态的函数地址晚绑定  -  运行阶段确定函数地址


当子类重写父类中的虚函数,并且父类指针或引用指向子类对象时,父类指针调用虚函数是子类重写的虚函数,而不是本身的虚函数。

当类中存在纯虚函数时,该类就是无法实例化对象的抽象类,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
纯虚函数语法:`virtual 返回值类型 函数名 (参数列表)= 0 ;

class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func() 
	{
		cout << "func调用" << endl;
	};
};

void test01()
{
	Base * base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;//记得销毁
}

int main() {

	test01();

	system("pause");

	return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————


6.类的底层原理

1.类底层结构
其实类是用结构体实现的,该结构体中保存着成员变量,成员函数的指针,静态成员函数和静态成员函数的指针等。
当我们实例化一个对象时,会复制一份类的数据,我们可以通过这份数据访问所有对象共同指向的成员函数和静态成员。但成员变量每个对象各自一份的,变量的位置是加上了this的偏移,所以每个对象的变量位置不相同。
静态成员函数和成员函数最大的不同是,静态成员函数不能访问非静态的成员变量,而非静态成员函数可以,这是因为非静态成员函数有默认的形参this,通过this找到非静态成员变量。
同样静态成员函数由于没有this指针,也不能给非静态成员函数赋值this,所以也不能调用非静态成员函数。

2.虚函数底层原理
当类中有虚函数时,类在底层的结构体就会有一个虚函数指针,指向虚函数表,表中的每一项都指向对应的虚函数。但子类重写了虚函数,子类将父类的数据继承下来,同时也继承了虚函数指针和虚函数表,子类也会将虚函数表中的重写的那一项指针指向新的虚函数,这样子类就只能访问子类重写的虚函数了。
当子类中有新的虚函数,就会往虚函数表中往下添加表项,并指向对应的虚函数。

结论:

(1) 对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。

(2) 对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。

(3) 虚函数表中,先存放父类的虚函数,再存放子类的虚函数

(4) 如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。

(5) 对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。

当父类指针指向子类的对象时,能获取数据是因为,子类对象先存放了父类的数据,并将重写的虚函数进行了替换。

在这里插入图片描述

  • 0
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
doc格式,60多页吧,几百道题吧,都有答案吧,看好在下!部分:1.求下面函数的返回值(微软)int func(x) { int countx = 0; while(x) { countx ++; x = x&(x-1); } return countx; } 假定x = 9999。 答案:8思路:将x转化为2进制,看含有的1的个数。2. 什么是“引用”?申明和使用“引用”要注意哪些问题?答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。3. 将“引用”作为函数参数有哪些特点?(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。4. 在什么时候需要使用“常引用”? 如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;例1int a ;const int &ra=a;ra=1; //错误a=1; //正确 例2string foo( );void bar(string & s); 那么下面的表达式将是非法的:bar(foo( ));bar("hello world"); 原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const 。5. 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!注意事项:(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。 (2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。(4)流操作符重载返回值申明为“引用”的作用:流操作符<>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。例3#i nclude int &put(int n);int vals[10];int error=-1;void main(){put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10; put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20; cout<<vals[0]; cout<<vals[9];} int &put(int n){if (n>=0 && n<=9 ) return vals[n]; else { cout<<"subscript error"; return error; }} (5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。6. “引用”与多态的关系?引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。例4Class A; Class B : Class A{...}; B b; A& ref = b;7. “引用”与指针的区别是什么?指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传ref和pointer的区别。8. 什么时候需要“引用”?流操作符<>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。以上 2-8 参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx9. 结构与联合有和区别?1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。 2. 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。10. 下面关于“联合”的题目的输出?a)#i nclude union{int i;char x[2];}a;void main(){a.x[0] = 10; a.x[1] = 1;printf("%d",a.i);}答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)………………

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值