从C到C++

本文深入讲解C++的基础知识,包括"cout <<"和"cin >>"的输入输出,引用的概念与使用,"const"关键字的应用,动态内存分配,以及函数、类与对象的相关概念。重点介绍了构造函数、复制构造函数、析构函数的作用及其使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、基础进阶部分

1、基本常识及概念扩充:

1)、“cout <<” 、"cin >>"与“输入”、“输出”
#include <iostream>
using namespace std;

int main()
{
	int i = 0;
	cout << "Hello World!" << endl;
	cin >> i;
	
	return 0;
}

1、“cout <<”:
"cout"实际上是C++定义的对象名,称为“输出流对象”。而"<<"则是插入运算符,配合“cout”使用,实现将"<<"后的数据插入到输出队列中(输出流),进而将其输出到显示屏上。

2、“cin >>”:
"cin"同理"cout",是C++定义的“输入流对象”。">>"则称作“提取运算符”,配合"cin"使用,实现从输入设备中(如键盘)提取输入数据送入输入流,进而获取数据。

3、“endl”

2、引用(Reference)概念以及使用:

1)、引用概念
#include <iostream>
using namespace std;

int main()
{
	int n = 4;
	int &r = n;

	cout << "n = " << n << endl;
	//输出“n = 4”
	cout << "r = " << r << endl;
	//输出“r = 4”

	double a = 4, b = 5;
	double & r1 = a;
	double & r2 = r1;// r2也引用 a 
	r2 = 10;
	cout << a << endl;// 输出 10
	
	r1 = b;// r1并没有引用b
	cout << a << endl;
	
	return 0;
}

这段代码,定义了一个整型变量“n”,并初始化。同时,又定义了一个整型引用变量“r”,并将其初始化为引用变量“n”。
引用就相当于被用作初始化变量的“别名”。引用的出现,扩充了函数传递数据的功能。从指针变量传地址,上升到引用传变量。

注意:引用使用

1、定义引用时一定要将其初始化成引用某个变量。
2、初始化后,它就一直引用该变量,不会再引用别的变量了。
3、引用只能引用变量,不能引用常量和表达式。

2)、将变量名作为实参
#include <iostream>
using namespace std;

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

int main(int argc, char *argv[])
{
	int i = 3, j = 5;

	swap(i, j);

	cout << "i = " << i << endl;
	cout << "j = " << j << endl;
	return 0}

利用引用作为函数参数,以扩充函数传递数据的功能。同时,减少了对指针以及相关指针运算符号的使用。

3)、定义常引用
	int a = 0;
	const int &r = a;

定义引用时,在最前面加上“const”关键字,即为常引用。此时,“r”的数据类型为“const int &”。

	int a = 1;
	const int &r = a;
	r = 10;//错误
	a = 11;//正确

不能通过常引用去修改其所引用的变量的值。

注意:const T & 和T & 是不同的类型!!!:

T & 类型的引用或T类型的变量可以用来初始化 const T & 类型的引用。
const T 类型的常变量和const T & 类型的引用则 不能用来初始化T &类型的引用,除非进行强制类型 转换。

引用原本的变量,而不去开辟一个新地址,很大程度上减少了内存的开销,是十分经济的做法。

3、“const”关键字的用法:

1)、定义常量
	const int MAX_VAL = 23;
	const String str = "Hello World!";
2)、定义常指针
	int n, m;
	const int *p = &n;
	*p = 5;		//Error
	n = 4;		//PASS
	p = &m;		//PASS,常量指针的指向可以改变

不能通过常量指针修改其所指向的内容。

	const int *p1;
	int *p2;
	
	p1 = p2;		//PASS
	p2 = p1;		//Error
	p2 = (int *) p1;	//PASS,强制类型转换

不能把常量指针赋值给非常量指针,反过来非常量指针可以给常量指针赋值.

	void myPrintf(const char *p)
	{
		strcpy(p, "this");	//Error
		printf("%s", p);	//PASS
	}

函数参数为常量指针时,可以避免函数内部改变参数指针所指位置的内容。

3)、定义常引用
	int n = 0;
	const int &r = n;
	r = 5;		//Error
	n = 4;		//PASS

不能通过常引用修改其引用的变量。

4、动态内存分配:

1)、对变量的分配
	int *p = new int;
	*p = 5;

动态分配出一片大小为 sizeof(int) 字节的内存空间,并将该内存空间的起始地址赋值给p。再做 *p = 5 进行整型变量的赋值。

2)、对数组的分配
	int *p = new int[10];
	p[0] = 1;		//数组中单一元素赋值
	p[20] = 3;		//Build可以通过,Running时会发生数组越界的情况

常用于创建初始长度未知的数组,输入数组长度,然后通过动态分配内存建立一个刚好的数组。

3)、通过 delete 释放 new 动态分配出的内存
	int *p = new int;
	*p = 5;

	delete p;		//释放指针p所指向的new出来的整型变量内存空间

	int *pn = new int[10];
	pn[0] = 1;

	delete[] pn;		//释放指针pn所指向的new出来的整型数组内存空间

切记,delete 数组时,一定要加上 [ ] 方括号才可以。否则只会释放 pn[0]。

5、函数:

1)、内联函数
	inline int Max(int a, int b)
	{
		if (a > b)
		{
			return a;
		}
		else
		{
			return b;
		}
	}

为了减少简短的函数在被调用时的时间开销,在C++中,引入了 “内联函数” 机制。编译器在处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。

2)、函数重载
	int Max(double f1, double f2){}		// 1
	int Max(int n1, int n2){}		// 2
	int Max(int n1, int n2, int n3){}	// 3

	Max(3.4, 2.5);		//调用1
	Max(2, 4);		//调用2
	Max(1, 2, 3);		//调用3
	Max(3, 2.4);		//Error,存在二义性

一个或者多个函数,在名字相同的前提下,函数的参数个数或者参数类型不同,叫做函数的重载。函数重载的出现,使得函数的命名变得简单。编译器会根据调用语句中的实参个数和类型判断应该调用哪个函数。

3)、函数的缺省参数
	void fun(int x1, int x2 = 2, int x3 = 3){}

	fun(10);		//x1 = 10, x2 = 2, x3 = 3
	fun(10, 8);		//x1 = 10, x2 = 8, x3 = 3
	fun(10, , 8);		//Error,只能最右边的连续若干个参数缺省

C++中,定义函数的时候,可以让函数“最右边的连续若干个参数”有缺省值,那么调用函数的时候,若对应位置不写参数,参数就是缺省值。其目的在于提高程序的可扩充性。其意义在于,倘若一个写好的函数需要添加新的参数,而原先调用该函数的语句又未必会使用新增加的参数,那么为了避免对原先函数调用语句的修改,就可以使用缺省参数。

void add(int a = 0, int b = 0);

void add(int a, int b)
{
	A = a;
	B = b;
}

对于函数声明与定义分开时,应当将缺省参数写入声明中。
注意:
1、参数缺省,采取的是从右向左的顺序,且必须连续
2、调用实参对形参的初始化必须时从左向右
3、使用缺省参数时,避免出现重载时的二义性

二、类与对象

1、类与对象

1)、定义
#include <iostream>
using namespace std;

class Student
{
	public:
		void addName();
		void addAge();
		void addSex();
		void addStudentNumber();
		
		void getName();
		void getAge();
		void getSex();
		void getStudentNumber();
	private:
		char name[10];
		int age;
		char sex;
		int studentnumber;
};

int main()
{
	Student stu;
	return 0;
}

类的定义不过多赘述,Java有些东西可以照搬。
注意:
1、C++对类的定义,千万不要缺失了最后的分号。原因是class就是原来的struct,或者说C++对于类定义当成了一个固定语句。
2、new 在C++中,可不是用来定义对象的。用于开辟内存的。所以C++定义新的对象就是上面的那种形式。

2)、对象的内存分配

和结构变量一样,对象所占用的内存空间大小,不单纯是所有成员变量之和的大小,具体涉及到内存的对齐问题。所以,想知道占了多少内存,还是用 sizeof 去求吧。

每个对象拥有自己的内存空间,改变一个对象的一个私有成员变量,不会波及到另一个对象。

3)、对象间的运算

同结构变量一样,对象之间可以用“=”进行赋值,但是不能使用“==”、“!=”、“>”、“<”、“>=”、“<=”做比较运算,除非这些运算符经过了“重载”。

2、使用类的成员变量和成员函数

class CRectangle
{
public:
	int w, h;
	int Area()
	{
		return w * h;
	}
	int Perimeter()
	{
		return 2 * (w + h);
	}
	void Init(int w_, int h_)
	{
		w = w_;
		h = h_;
	}
};
1)、对象名.成员名
	CRectangle r1, r2;
	r1.w = 5;
	r2.Init(5,4);
2)、指针->成员名
	CRectangle r1, r2;
	CRectangle *p1 = &r1;
	CRectangle *p2 = &r2;

	p1->w = 5;
	p2->Init(5,4);	
3)、引用名.成员名
	CRectangle r1;
	CRectangle &rr = r2;
	rr.w = 5;
	rr.Init(5,4);	

3、类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员的可被访问的范围:

private:私有成员,只能在成员函数内访问
public:共有成员,可以在该对象存在的任何地方通过对象访问
protected:保护成员,对于本类抽象出的对象,访问权限跟私有成员一样

1、以上三种关键字的出现次数与先后顺序都没有限制
2、class定义类,如果没有访问范围关键字修饰,默认作为 private成员处理
3、struct定义类,则默认作为 public成员处理

class Test
{
private:
	//私有属性的成员变量和函数
public:
	//共有属性的成员变量和函数
protected:
	//保护属性的成员变量和函数
};

4、成员函数的重载及参数缺省

跟普通函数,类的成员函数也可以重载和缺省参数

#include <iostream>
using namespace std;

class Location
{
private:
	int a, b;
public:
	//缺省参数的声明
	void init(int A = 0, int B = 0);

	//重载
	void valueA(int val)
	{
		A = val;
	}
	int valueA()
	{
		return a;
	}
};

//缺省参数函数的定义
void Location::init(int A, int B)
{
	a = A;
	b = B;
}

int main()
{
	Location A,B;
	A.init(5);
	A.valueA(5);
	cout << A.valueA();
	
	return 0;
}

注意:要避免使用缺省参数时出现的重载二义性

class Test
{
private:
	int a = 1;
public:
	void valueA(int A = 0)//删除“ = 0”,正常调用 int valueA()
	{
		a = A;
	}
	int valueA()
	{
		return a;
	}
};

int main()
{
	Test t;
	t.valueA();
}

Error: call of overloaded 'valueA()' is ambiguous
G++在编译时,就会给出上面的错误,说明使用了参数缺省后,无法判断调用哪个 valueA

5、构造函数

1)、构造函数基本概念
  • 构造函数是成员函数的一种,名字与类的名字相同,可以有参数,没有返回值
  • 作用是对对象进行初始化,例如给成员变量赋初值
  • 如果定义类时没有写构造函数,则编译器会生成一个默认的无参数的构造函数,但是其不会做任何操作。如果定义了构造函数,编译器则不会生成无参数的构造函数
  • 对象生成时,构造函数被自动调用,且只调用一次。生成后,不会再调用构造函数。
  • 一个类可以有多个构造函数(函数的重载
#include <iostream>
using namespace std;
class Test
{
private:
	int number = 0;
	int value = 0;
	int count = 0;
	int time = 0;
	
public:
	Test(int N);
	Test(int N, int V);
	Test(Test &a, Test &b);
	void printTest();
};

Test::Test(int N)
{
	number = N;
}
Test::Test(int N, int V)
{
	number = 2 * N;
	value = 2 * V;
}
Test::Test(Test &a, Test &b)
{
	number = a.number + b.number;
	value = a.value + b.value;
	count = a.count + b.count;
	time = a.time + b.time;
}
void Test::printTest()
{
	cout << number << " " << value << " " << count << " " << time << endl;
}

int main()
{
	Test T(1);
	Test E(2, 3);
	Test S(T, E);
	
	cout << "T: " << endl;
	T.printTest();
	
	cout << "E: " << endl;
	E.printTest();

	cout << "S: " << endl;
	S.printTest();

	getchar();
	getchar();

	return 0;
}

构造函数最好是“public”,“private”不能直接用来初始化对象

#include <iostream>
using namespace std;

class CSample
{
private:
	CSample(){ }
};
int main()
{
	CSample Obj; //Error,唯一构造函数是private
	return 0;
}
2)、构造函数在数组中的使用
#include <iostream>
using namespace std;

class CSample
{
private:
	int x;
public:
	CSample()
	{
		cout << "Constructor 1 Called" << endl;
	}
	CSample(int n)
	{
		x = n;
		cout << "Constructor 2 Called" << endl;
	}
};

int main()
{
	CSample array1[2];
	cout << "step1"<<endl;
	//Constructor 1 Called
	//Constructor 1 Called
	//step1
	
	CSample array2[2] = {4,5};
	cout << "step2"<<endl;
	//Constructor 2 Called
	//Constructor 2 Called
	//step2 
	
	CSample array3[2] = {3}; 
	cout << "step3"<<endl;
	//Constructor 2 Called
	//Constructor 1 Called
	//step3 

	CSample *array4 = new CSample[2];
	//Constructor 1 Called
	//Constructor 1 Called

	delete []array4;
	return 0;
}

使用类定义对象数组时,实际上是同时定义了数组上限个数的该类的对象,所以每个定义才会出现调用两次构造函数的情况

class  Test
{
public:
	Test(int n) {}			//(1)
	Test(int n, int m) {}		//(2)
	Test() {}			//(3)
};
int main()
{
	Test  array1[3] = {1, Test(1,2)};
	// 三个元素分别用(1),(2),(3)初始化
	
	Test  array2[3] = {Test(2,3), Test(1,2), 1};
	// 三个元素分别用(2),(2),(1)初始化
	
	Test  *pArray[3] = {new Test(4), new Test(1,2)};
	//两个元素分别用(1),(2) 初始化
	return 0;
}

6、复制构造函数

1)、概念

在C++中,有时需要用到多个完全相同的对象,如果使用直接定义多个对象的方法,比较麻烦。同时呢,有时又需要将对象某个瞬间的状态值保留下来。于是,复制构造函数孕育而生。

#include <iostream>
using namespace std;

class Test
{
private:
	int test = 0;
	int value = 0;
	
public:
	Test(int t, int v)
	{
		test = t;
		value = v;
	}
	Test(const Test &T)	//将该复制构造函数注释掉,结果依旧不变
	{
		test = T.test;
		value = T.value;
	}
	void testPrint()
	{
		cout << "test: " << test << endl;
		cout << "value: " << value << endl;
	}
};

int main()
{
	Test T(2, 3);

	Test S(T);

	S.testPrint();
	
	return 0;
}

结果输出的是,“test: 2”,“value:3”。表明,对象S通过调用复制构造函数,变成了对象T的复制品。
注意:
1、复制构造函数,只有一个参数,即——对同类的对象的引用
2、格式可以是 class::class(class &c) 或者 class::class(const class &c),二者选一。但是,后者的const前缀,可以让其将常量对象作为实参
3、如果没有定义复制构造函数,编译器会生成默认的复制构造函数,也会完成复制功能
4、如果定义了自己的复制构造函数,编译器不会再生成默认的复制构造函数

2)、复制构造函数起作用的三种情况
  • (1)、当用一个已经初始化的对象去初始化同类另一个新定义的对象时
	Test T(2, 3);
	Test S(T);
	Test E = T;
  • (2)、如果某函数有一个参数是类A的对象,那么,在这个函数被调用时,类A 的复制构造函数将被调用
void Func(A a2){}

int main()
{
	A a1
	Func(a1);
	
	return 0;
}
  • (3)、如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用
class A
{
public:
	int v;
	A(int n){v = n;}
	A(const A &a)
	{
		v = a.v;
		cout << "Copy constructor called" << endl;
	}
};

A Func()
{
	A b(4);
	return b;
}

int main()
{
	cout << Func().v << endl;
	return 0;
}

// 输出结果:
// Copy constructor called
// 4

注意:对象之间的相互赋值,并不会导致复制构造函数被调用
调用复制构造函数时,由于有额外开销,可以考虑使用" class & "对象的引用类型作为构造函数

7、类型转换构造函数

  • 定义转换构造函数的目的是为了实现类型的自动转换
  • 只有一个参数,且参数不是本类对象的常引用,而是一个单独的基本变量形参
  • 需要时,编译器会自动调用该函数,将形参放入一个临时对象中
class Test
{
public:
	int a = 0, b = 0;
	Test(){}
	Test(int A)
	{
		a = A;
		b = -1;
	}
}

int main()
{
	Test T1;
	T1 = 9;

	cout << "T1: " << "a = " << T1.a << "b = " << T1.b << endl;
}

这段程序,最后会输出“a = 9 b = -1”
类型转换构造函数,其实也就是某一个重载的构造函数。只要有一个含有一个基本类型参数的构造函数,其所充当的,一个是被重载的构造函数,再一个就是类型转换构造函数

8、析构函数

  • 名字与类名相同,在前面加‘~’, 没有参数和返回值,一 个类最多只能有一个析构函数
  • 析构函数对象消亡时即自动被调用。可以定义析构函数来在 对象消亡前做善后工作,比如释放分配的空间等
  • 如果定义类时没写析构函数,则编译器生成一个缺省的析构函数,但是,这个析构函数什么也不做,函数本身是空的
  • 如果定义了析构函数,则编译器不生成缺省析构函数
#include <iostream>
using namespace std;

class Test
{
public:
	Test()
	{
		T = 10;
	}
private:
	int T = 0;
};
int main()
{
	Test test;
	return 0;
}

上面这个例子,没有自己定义析构函数,同时,类定义的对象在消失后,其占用的空间被自动回收,析构函数没有任何作用

#include <iostream>
using namespace std;

struct Node
{
	int data;
	struct Node *next;
};

class LinkQueue {
private:
	Node *front, *rear;
public:
	LinkQueue()
	{
		Node *p = new Node;
		p->next = NULL;
		front = rear = p;
	}
	~LinkQueue()
	{
		Node *p = front;
		while(front != NULL)
		{
			front  = front->next;
			delete p;
			p = front;
		}
	}
};

int main()
{
	LinkQueue linkqueue;
	
	return 0;
}

虽然主函数只有两句,被定义的对象,却调用了一次构造函数和一次析构函数。初始对象执行构造函数,动态分配了一个内存结点;主函数进行至“return 0”,标志该对象完成了使命,因此会再度执行析构函数,进行善后回收处理,将动态分配的内存做回收释放处理
析构函数的写与不写是相对的,倘若类的成员变量均为基本类型变量,可以完全由编译器生成默认的缺省析构函数,进而什么都不做;如果出现动态内存分配这样的情景,倘若什么都不做,就会一直占用系统内存,进而产生问题,此时就需要手动定义析构函数,做相应的善后工作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值