c正常的字符串都会留出一个空间给\0
在C语言中,当我们将一个字符数组用作字符串时,我们通常会确保数组的大小至少比字符串中实际字符的数量多一个字符的空间,以便能够存储字符串的终结符——空字符('\0'
)。
这个空字符('\0'
)对于字符串的结束标记非常重要,因为它告诉程序字符串在哪里结束。没有它,程序就无法知道字符串何时结束,这可能会导致它继续读取内存中的字符,直到偶然遇到一个空字符为止,这可能导致未定义行为,包括读取未初始化的内存、越界访问等。
例如,如果我们想要存储字符串 "hello",我们应该声明一个至少包含6个字符的字符数组(5个字符用于存储 "hello",加上1个字符用于存储 '\0'
):
char str[6] = "hello"; |
或者,如果我们单独初始化数组的每个元素,我们也应该记得添加空字符:
char str[6]; | |
str[0] = 'h'; | |
str[1] = 'e'; | |
str[2] = 'l'; | |
str[3] = 'l'; | |
str[4] = 'o'; | |
str[5] = '\0'; // 不要忘记这个! |
在上面的例子中,即使我们没有显式地初始化 str[5]
为 '\0'
,编译器在将字符串字面量 "hello"
赋值给 str
时也会自动添加空字符。但是,如果我们以逐个字符的方式初始化数组,并且忘记了空字符,那么我们就需要自己添加它。
因此,当我们处理字符串时,确保为终结的空字符留出空间是一个好习惯。
c的哈希库使用
1. 引入哈希库并初始化
首先,需要引入哈希库,比如uthash库,并定义哈希表的结构体。在uthash中,每个哈希元素都需要一个UT_hash_handle
类型的字段来维护哈希表的链接。
#include "uthash.h"
typedef struct {
int key; // 关键字
int value; // 值
UT_hash_handle hh; // uthash所需的字段
} Hash;
Hash *hashtable = NULL; // 初始化哈希表为空
2. 插入数据
使用哈希库提供的函数(如uthash中的HASH_ADD_INT
)来插入数据。如果关键字已存在,则更新其值;如果不存在,则创建新元素。
void hash_insert(int key, int value) {
Hash *it = NULL;
HASH_FIND_INT(hashtable, &key, it); // 查找关键字
if (it == NULL) { // 如果未找到,则插入新元素
Hash *tmp = malloc(sizeof(Hash));
tmp->key = key;
tmp->value = value;
HASH_ADD_INT(hashtable, key, tmp); // 插入新元素
} else { // 如果已存在,则更新值
it->value = value;
}
}
3. 查找数据
使用哈希库提供的查找函数(如uthash中的HASH_FIND_INT
)来查找数据。如果找到,则返回对应的哈希元素指针;如果未找到,则返回NULL。
Hash *hash_find(int key) {
Hash *tmp = NULL;
HASH_FIND_INT(hashtable, &key, tmp); // 查找关键字
return tmp; // 返回找到的哈希元素指针或NULL
}
4. 删除数据
使用哈希库提供的删除函数(如uthash中的HASH_DEL
)来删除数据。传入要删除的哈希元素的指针,并从哈希表中移除该元素。
int hash_delete(int key) {
Hash *it = NULL;
HASH_FIND_INT(hashtable, &key, it); // 查找关键字
if (it != NULL) { // 如果找到,则删除
HASH_DEL(hashtable, it);
free(it); // 释放内存
return 1; // 删除成功
}
return 0; // 删除失败
}
unity的SceneManager
Unity的SceneManager是Unity引擎中用于管理游戏场景的静态类,它提供了一系列的方法来实现场景的加载、卸载、切换等操作。以下是对Unity SceneManager的详细介绍:
一、基本功能
- 场景加载:使用
SceneManager.LoadScene()
或SceneManager.LoadSceneAsync()
来加载场景。LoadScene()
是同步方法,会阻塞游戏直到场景加载完成;而LoadSceneAsync()
是异步方法,允许游戏在场景加载过程中继续运行,通常用于实现加载屏幕或进度条。 - 场景卸载:使用
SceneManager.UnloadScene()
或SceneManager.UnloadSceneAsync()
来卸载场景。这有助于管理游戏内存,避免不必要的资源占用。 - 获取场景信息:可以通过
SceneManager.GetActiveScene()
获取当前激活的场景,或者使用SceneManager.GetSceneByName()
、SceneManager.GetSceneByPath()
、SceneManager.GetSceneByBuildIndex()
等方法根据名称、路径或构建索引获取特定场景的信息。
二、常用方法
- 加载场景:
LoadScene(string sceneName)
:根据场景名称加载场景。LoadScene(int sceneBuildIndex)
:根据场景的构建索引加载场景。LoadSceneAsync(string sceneName, LoadSceneMode mode = LoadSceneMode.Single)
:异步加载场景,可以指定加载模式(如是否替换当前场景等)。
- 卸载场景:
UnloadScene(Scene scene)
:卸载指定的场景。UnloadSceneAsync(Scene scene)
:异步卸载场景。
- 获取场景信息:
GetActiveScene()
:获取当前激活的场景。GetSceneByName(string name)
:根据名称获取场景。GetSceneByPath(string path)
:根据路径获取场景。GetSceneByBuildIndex(int index)
:根据构建索引获取场景。
三、高级功能
- 场景光照贴图:使用
SceneManager.SetLightmapPreviewTexture()
可以设置场景的光照贴图预览。 - 场景雾效:通过
SceneManager.SetFog()
可以设置场景的雾效,包括雾的模式、颜色、密度等。 - 场景天空盒:使用
SceneManager.SetActiveSceneSkybox()
可以设置当前激活场景的天空盒。
四、场景管理提升游戏体验
- 创建过渡场景:为了避免场景切换时的黑屏或卡顿,可以创建一个过渡场景,在加载新场景时使用过渡场景进行过渡,提升游戏的流畅性和用户体验。
- 场景标志与控件:为不同的场景设置不同的标志,并在游戏中添加控件(如按钮)来控制场景间的切换,使游戏流程更加清晰和可控。
- 场景加载后的操作:可以使用
SceneManager.sceneLoaded
事件或OnSceneLoaded
方法,在新场景加载完成后执行一些特定的操作,如播放背景音乐、初始化变量等。
五、一定要放scenebuilding吗
在Unity中,如果你想要通过SceneManager.LoadScene()
或SceneManager.LoadSceneAsync()
等方法来加载场景,并不一定要将想要加载的场景放在Scene Building(构建设置)中。但是,将场景添加到Scene Building中有其特定的优势和必要性,尤其是在进行游戏发布和打包时。
场景放在Scene Building中的优势
- 游戏发布和打包:当你准备将游戏发布到不同平台时,Unity会根据Scene Building中的设置来打包游戏。只有添加到Scene Building中的场景才会被包括在最终的游戏包中。
- 场景顺序和依赖:在Scene Building中,你可以设置场景的加载顺序和依赖关系,这对于多场景游戏来说非常重要,可以确保场景按照正确的顺序加载,并处理场景间的依赖关系。
- 性能优化:通过Scene Building,你可以对场景进行优化,例如通过调整场景的加载顺序来减少加载时间,或者通过禁用不必要的场景来减少内存占用。
不放在Scene Building中的情况
虽然不将场景放在Scene Building中也可以通过编程方式加载(例如,从AssetBundle中加载场景),但这通常用于更高级的场景管理策略,如动态加载场景、按需加载场景等。这些策略在大型游戏或需要高度灵活性的应用中非常有用,但也需要更多的编程工作和资源管理。
结论
综上所述,虽然不一定要将想要加载的场景放在Scene Building中,但这样做对于游戏发布、打包、性能优化等方面都是非常有益的。如果你只是在进行简单的场景切换或测试,并且不需要将游戏发布到外部平台,那么你可能不需要将场景添加到Scene Building中。然而,在大多数情况下,将场景添加到Scene Building中是推荐的做法。
Hashtable
(非泛型)
在C#中,Hashtable
是一种用于存储键值对的集合,它属于 System.Collections
命名空间。与 Dictionary<TKey, TValue>
类似,但有一些关键的区别。关于 Hashtable
的成员和特性,以下是一些详细的说明:
1. 成员
- 键(Key)和值(Value):与
Dictionary
一样,Hashtable
也存储键值对,但键和值都必须是object
类型,这意味着它们可以是任何非null
的对象。 - 容量(Capacity):虽然
Hashtable
没有直接暴露一个名为Capacity
的属性来查询当前容量,但它确实在内部维护了一个数组来存储键值对。当元素数量超过当前容量时,Hashtable
会自动扩容。 - 方法:
Hashtable
提供了一系列方法来管理其成员,包括Add
(添加键值对)、Remove
(删除具有指定键的元素)、Clear
(移除所有元素)、ContainsKey
(检查键是否存在)等。 - 属性:
Hashtable
的主要属性包括Count
(包含的元素数量)、IsFixedSize
(指示Hashtable
是否具有固定大小,对于Hashtable
来说,这个属性总是返回false
)、IsReadOnly
(指示Hashtable
是否是只读的,对于Hashtable
来说,这个属性也是返回false
)、IsSynchronized
(指示Hashtable
是否是同步的,即是否可以在多线程环境中安全使用)等。但请注意,虽然Hashtable
提供了IsSynchronized
属性,但它并不保证完全的线程安全;对于多线程场景,建议使用ConcurrentDictionary
。
2. 特性
- 非泛型:与
Dictionary<TKey, TValue>
相比,Hashtable
不是泛型的,因此它只能存储object
类型的键和值。这意呀着在存储和检索值时需要进行类型转换,这可能会引入错误并降低性能。 - 线程安全性:虽然
Hashtable
的成员是线程安全的(即单个操作如添加、删除或访问是原子的),但整个集合并不是线程安全的。如果多个线程同时写入Hashtable
,可能会导致数据不一致。 - 性能:由于
Hashtable
存储的是object
类型的键值对,因此它需要进行装箱和拆箱操作,这可能会影响性能。相比之下,Dictionary<TKey, TValue>
由于是泛型的,因此避免了装箱和拆箱,通常具有更好的性能。
3. 使用场景
- 当需要存储任意类型的键值对,并且不介意装箱和拆箱的性能开销时,可以使用
Hashtable
。 - 对于需要泛型支持、更好的性能和类型安全的场景,建议使用
Dictionary<TKey, TValue>
。
综上所述,Hashtable
在C#中是一个功能强大的集合类,但它的一些限制(如非泛型、可能的性能开销和不完全的线程安全性)使得它在许多情况下可能不是最佳选择。在选择使用 Hashtable
还是 Dictionary<TKey, TValue>
时,应根据具体的应用场景和需求进行权衡。
字典
在C#中,字典(Dictionary<TKey, TValue>
)类确实有一个名为 ContainsKey
的方法,用于检查字典中是否包含具有指定键的元素。这个方法返回一个布尔值(bool
),如果字典包含具有指定键的元素,则为 true
;否则为 false
。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个字典实例
Dictionary<string, int> ages = new Dictionary<string, int>();
// 向字典中添加一些键值对
ages.Add("Alice", 30);
ages.Add("Bob", 25);
ages.Add("Charlie", 35);
// 检查字典中是否包含指定的键
string keyToCheck = "Bob";
if (ages.ContainsKey(keyToCheck))
{
Console.WriteLine($"Found {keyToCheck} in the dictionary.");
// 也可以获取该键对应的值
int age = ages[keyToCheck];
Console.WriteLine($"{keyToCheck} is {age} years old.");
}
else
{
Console.WriteLine($"{keyToCheck} not found in the dictionary.");
}
// 尝试查找一个不存在的键
string nonExistentKey = "David";
if (!ages.ContainsKey(nonExistentKey))
{
Console.WriteLine($"{nonExistentKey} not found in the dictionary.");
}
}
}
数组(Array)
- 数组是一种基础的数据结构,用于存储固定大小的同类型元素的集合。
- 数组的元素可以通过索引(通常是整数)来访问,索引从0开始。
- 一旦数组被创建,其大小就是固定的,即不能动态地添加或删除元素(尽管可以修改元素的值)。
- 数组在内存中是连续存储的,这通常使得访问元素非常快速。
字典(Dictionary)
- 字典是一种集合,用于存储键值对,其中每个键都是唯一的,并与一个值相关联。
- 字典的大小不是固定的,可以动态地添加、删除和修改键值对。
- 字典中的元素(键值对)不是通过索引来访问的,而是通过键来访问。
- 字典内部通常使用哈希表来实现,这使得查找、添加和删除操作都非常快(平均时间复杂度为O(1))。
- 字典中的键和值可以是不同的数据类型,但键必须是唯一的,并且必须实现
GetHashCode
和Equals
方法,以便字典能够正确地存储和检索键值对。
字典的成员
-
键(Key):字典中的每个元素都由一个键来唯一标识。键用于检索、添加、删除或更新字典中的元素。键的类型由
TKey
泛型参数指定,它必须是一个可比较的类型,通常是一个实现了IEquatable<T>
接口和GetHashCode
方法的类型,以便字典能够正确地存储和检索键值对。 -
值(Value):与键相关联的数据存储在值中。值的类型由
TValue
泛型参数指定,它可以是任何类型,包括自定义类型。同一个字典中的值可以重复,但键必须是唯一的。 -
键值对(KeyValuePair<TKey, TValue>):字典实际上是由一系列键值对组成的集合。在C#中,
KeyValuePair<TKey, TValue>
是一个结构体,用于表示单个键值对。虽然你通常不会直接创建KeyValuePair<TKey, TValue>
的实例来添加到字典中(而是使用字典的Add
方法或初始化器),但你可以在遍历字典时遇到它,例如在使用foreach
循环遍历字典的Keys
、Values
或KeyValuePair
时。 -
容量(Capacity)不是数量:虽然字典的容量不是一个直接暴露给用户的成员(即没有
Capacity
属性),但它确实在内部用于管理存储键值对的数组的大小。当添加的元素数量超过当前容量时,字典会自动增加其内部数组的容量,这可能会导致性能开销。但是,你也可以通过调用Dictionary<TKey, TValue>.TrimExcess
方法来减少未使用的容量,从而优化内存使用。这里的“不能查”指的是在C#的
Dictionary<TKey, TValue>
类中,没有直接提供一个名为Capacity
的属性来让用户查询当前字典的内部数组容量。这是因为字典的设计者认为,让用户直接管理容量(比如通过查询或设置容量)可能会破坏字典内部优化和性能的特性。然而,这并不意味着你完全无法了解或影响字典的容量。实际上,当你向字典中添加元素时,如果元素数量超过了当前内部数组的容量,字典会自动进行扩容(即增加内部数组的大小),以容纳更多的元素。这个扩容过程可能会导致一定的性能开销,因为它涉及到内存的重新分配和元素的复制。
-
方法:字典提供了多种方法来管理其成员,包括添加(
Add
)、删除(Remove
)、检查键是否存在(ContainsKey
)、检查值是否存在(通过ContainsValue
,但请注意,由于值的非唯一性,这可能会比检查键存在更耗时)、获取值(TryGetValue
,这是一种更安全的获取值的方式,因为它不会抛出异常,而是返回一个布尔值来指示是否找到了键,并通过输出参数返回对应的值)、清除所有元素(Clear
)等。 -
属性:字典的主要属性包括
Keys
(返回包含字典中所有键的Dictionary<TKey, TValue>.KeyCollection
)、Values
(返回包含字典中所有值的Dictionary<TKey, TValue>.ValueCollection
)和Count
(返回字典中包含的键值对数量)。请注意,Keys
和Values
返回的集合是只读的,但你可以通过字典本身来修改这些集合背后的键值对。
C#和C在函数参数传递的不同
C语言:
- C语言中的函数参数传递主要是基于值传递(Pass by Value)。无论是基本数据类型(如int、float)还是指针类型,传递给函数的都是参数的一个副本。对于指针类型,传递的是指针变量的副本,但这个副本仍然指向原始数据,因此可以通过指针来修改原始数据。
- C语言没有直接的引用传递机制(如C#中的ref或out关键字),但通过使用指针,可以达到类似引用传递的效果。
C#:
- C#提供了更灵活的参数传递机制,包括值传递(Pass by Value)和引用传递(Pass by Reference)。
- 对于值类型(如int、float、struct),默认情况下是按值传递。这意味着传递给函数的是参数的一个副本,函数内部对参数的修改不会影响到原始变量。
- 对于引用类型(如class、interface、delegate、数组),默认情况下是按引用传递。这意味着传递给函数的是对原始对象的引用,函数内部对对象的修改会影响到原始对象。但需要注意的是,如果在函数内部将引用指向了一个新的对象,那么原始引用将不会受到影响(因为它仍然指向原来的对象)。
- C#还提供了
ref
和out
关键字,允许程序员显式地按引用传递值类型,以及从函数中返回多个值(通过out
参数)。
C++:
在C++中,与c语言不同,存在引用传递&(也就是说c语言其实并没有&,&只属于c++)。
与值传递(pass by value)不同,引用传递不会创建参数的副本,而是直接传递参数的引用(或地址)。这意味着,函数内部对参数的任何修改都会影响到原始数据。
c#
函数传入的参数
值类型(Value Types)
对于值类型(如int
、float
、struct
等),当你将一个值类型变量作为参数传递给一个方法时,实际上传递的是该变量的一个副本。这意味着方法内对该参数的任何修改都不会影响原始变量。
void ModifyValue(int x) {
x = 5;
}
int originalValue = 10;
ModifyValue(originalValue);
// originalValue 仍然是 10
引用类型(Reference Types)
对于引用类型(如class
、interface
、delegate
、数组等),当你将一个引用类型变量作为参数传递给一个方法时,实际上传递的是对该变量的引用(或者说是指向该变量内存地址的指针)。这意味着方法内对该参数的任何修改都会影响到原始对象。
void ModifyObject(SomeClass obj) {
obj.SomeProperty = "Modified";
}
SomeClass originalObject = new SomeClass();
originalObject.SomeProperty = "Original";
ModifyObject(originalObject);
// originalObject.SomeProperty 现在是 "Modified"
特殊情况:ref
和out
关键字
C#提供了ref
和out
关键字,允许你按引用传递值类型。这意味着即使是值类型,你也可以让方法内对该参数的修改影响到原始变量。
void ModifyValueByRef(ref int x) {
x = 5;
}
int originalValue = 10;
ModifyValueByRef(ref originalValue);
// originalValue 现在是 5
UIInput成员
在NGUI(Next-Gen UI)框架中,UIInput
是一个用于处理用户输入(如文本输入)的组件。UIInput
继承自 UIWidget
,添加了对文本输入的支持。以下是一些 UIInput
在 NGUI 中常见的成员(属性和方法):
属性
value
: 字符串类型,表示输入字段的当前值。label
:UILabel
类型,用于显示输入字段的标签。inputType
: 枚举类型,定义了输入类型,如标准、密码、自动更正等。validation
: 字符串类型,用于正则表达式验证输入。onSubmit
: 当提交输入时调用的事件。onChange
: 当输入值改变时调用的事件。selected
: 布尔类型,表示输入字段是否被选中。isActive
: 布尔类型,表示输入字段是否处于激活状态。caretPosition
: 整型,表示光标在文本中的位置。
抽象类和方法
- 抽象类不能被实例化。
- 抽象类中可以包含抽象方法和非抽象方法。
- 继承自抽象类的子类必须实现所有的抽象方法,除非子类也被声明为抽象类。
- 在C#中,普通类(非抽象类)不能有抽象方法。
public abstract class Animal
{
// 抽象方法
public abstract void MakeSound();
// 非抽象方法(普通方法)
public void Sleep()
{
Console.WriteLine("The animal is sleeping.");
}
}
public class Dog : Animal
{
// 实现抽象方法
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
// 实现抽象方法
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}
class Program
{
static void Main(string[] args)
{
Dog myDog = new Dog();
myDog.MakeSound(); // 输出: Woof!
myDog.Sleep(); // 输出: The animal is sleeping.
Cat myCat = new Cat();
myCat.MakeSound(); // 输出: Meow!
myCat.Sleep(); // 输出: The animal is sleeping.
}
}
普通类virtual和抽象类
抽象类和普通类中的virtual
方法都允许子类提供具体的实现。但是,对于抽象方法,子类必须提供实现;而对于virtual
方法,子类可以选择重写它,也可以选择不重写(此时会使用基类的实现)。
如果你继承了一个抽象类,并且你不想(或不能)实现该抽象类中的所有抽象方法,那么你的子类也必须被声明为抽象类。这是C#语言规范的一部分,旨在确保类型系统的完整性和一致性。
抽象类不能被实例化,而普通类可以被实例化。这意味着,如果你想要一个不能直接被实例化的类,并且强制子类实现某些方法,你应该使用抽象类。
抽象方法强制子类提供实现,而virtual
方法不是必须的。子类可以选择不重写virtual
方法,但如果重写了,那么就会使用子类提供的实现。
抽象类通常用于定义一组接口(即抽象方法),这些接口将由非抽象子类实现。它们是一种模板或规范,用于指定子类应该提供哪些功能。
而普通类中的virtual
方法更多地用于提供一种在子类中可以选择性地修改或扩展的功能。
泛型约束
让泛型类型有限制 关键字:where
一共有六种
值类型 where T:struct
引用类型 where T:class
存在无参公共构造函数 where T:new()
某个类或其派生类 where T:类名
某个接口或其派生类 where T:接口名
另一个泛型类型或其派生类 where T:另一个泛型字母
杂
// 尝试查找一个不存在的键
string nonExistentKey = "David";
if (!ages.ContainsKey(nonExistentKey))
{
Console.WriteLine($"{nonExistentKey} not found in the dictionary.");
}
隐藏的面板不渲染,不算drawcall