Microsoft Visual C++ .NET 2003 Kick Start Chapter 3: The .NET Base Class Libraries





4.00/5 (2 votes)
Feb 18, 2004
22 min read

68508
An introduction to the .NET base class libraries.
![]() |
|
The .NET Base Class Libraries
In This Chapter
- Libraries Shared Across Languages
- Namespaces in C++
- The System Namespace
- Other Useful Namespaces
- In Brief
Libraries Shared Across Languages
When you write managed C++, you have access to all of the managed code libraries that come with the .NET Framework: the Base Class Libraries, ADO.NET, ASP.NET, and so on. These libraries are modern and powerful. They provide services, such as XML processing, that weren't even thought of when older libraries such as MFC and ATL were first written.
In sharp contrast to the historical capabilities of C++ and Visual Basic, on the .NET Framework these two languages share the same class libraries. Every library class and method that's available from Visual Basic is available from managed C++ and from C#. Every library class and method that's available from C# is available from Visual Basic and Managed C++.
The advantages of a shared class library are many. They include:
- Reduced learning time when moving from one .NET-supported language to another
- A larger body of samples and documentation, because these do not need to be language specific
- Better communication between programmers who work in different .NET-supported languages
The C++ Advantage - C++ can use the same class libraries as C# and VB.NET. Does it work the other way around? No. There are libraries of unmanaged code available from managed C++ that cannot be called from Visual Basic or C#—ATL and MFC are just two examples. However, it's unlikely that a Visual Basic or C# programmer would want to use those libraries, because their functionality is provided elsewhere; the capability to call them from managed C++ helps simplify a port from unmanaged to managed C++.
Working in Multiple Languages - I have a small consulting firm, and often our clients specify the programming language we are to use for a project. When several projects are on the go at once, I might switch languages several times in the course of a single day, as I help my associates with problems or track down the last few bugs in systems that are almost finished.
Before I started using the .NET Framework, about once a month I'd suffer a "brain freeze" and for a moment or two forget how to perform some really simple task, like determining whether a string contains a particular character, in the language of the moment. Usually at those times, my fingers would start typing the method or operator in randomly chosen other languages or libraries: Perl, Visual Basic, MFC, STL, Java—anything except the one I wanted. Now, with a common set of libraries across languages, I don't have to "context switch" nearly as often—and I avoid those "deer in the headlights" moments.
In the past, just because you knew how to perform a specific task, such as writing some text to a file in Visual C++, didn't mean you knew how to do that same task in Visual Basic. A great tutorial you found on database access in Visual Basic wasn't much help if you wanted to write your database application in Visual C++. Now, as long as you're working in managed C++, the walkthroughs, tutorials, and samples you find for any managed language—and you'll find plenty for Visual Basic and C#—are equally applicable to Visual C++. You'll have to translate the actual language elements, of course, but an explanation of a particular class or a method of that class is valid no matter which .NET supported language you intend to use.
Namespaces in C++
Namespaces are a C++ feature designed to eliminate name conflicts,
such as having two classes, each in different libraries, called
String
. Before namespaces were added to the language, library
developers tried to make their names unique by adding letters to them: One
developer's string class might be called GCString
, whereas another
developer might call it TKString
, the string class in MFC is called
CString
, and so on. This approach is ugly and reduces, but doesn't
prevent, name conflicts.
With namespaces, classes can have simple names. Name conflicts are much less likely, because in addition to a short or local name, classes have a fully qualified name that includes their namespace. Here's a slightly artificial example (normally namespaces are used in separate libraries, not jumbled together in one piece of code like this) that illustrates how they work:
namespace One
{
class Common
{
private:
int x;
public:
Common(int a): x(a) {}
int getx() {return x;}
};
void Do()
{
Common c(3);
Console::WriteLine(__box(c.getx()));
}
}
namespace Two
{
class Common
{
private:
double d1, d2;
public:
Common(double param1) : d1(param1),d2(param1) {}
double getd1() {return d1;}
double getd2() {return d2;}
};
void Do()
{
Common c(3);
String* output = String::Concat(__box(c.getd1()), S" " ,
__box(c.getd2()));
Console::WriteLine(output);
}
}
int _tmain()
{
//Common c(3); // ambiguous
One::Common c1(3);
Two::Common c2(3);
//Do(); //ambiguous
One::Do();
Two::Do();
return 0;
}
This code defines two namespaces, named One
and
Two
. In each namespace, there is a class called Common
and a function called Do()
. Inside the namespace, there's no
problem referring to Common
just using its short or local name. The
two Do()
functions accomplish this without error; each is working
with the Common
class from its own namespace.
The main function, _tmain()
, cannot refer to Common
or to Do()
using a short name. (The two lines of code commented out
in _tmain()
cause compiler errors.) It has to use the fully
qualified name: the namespace name and the class name, separated by the
scope-resolution operator (::
).
A using
statement allows you to refer to a class with
only its short name. It does not cause the compiler or linker to include any
files that otherwise wouldn't have been included in your build; it's just a
convenience to reduce typing. The example main function can be rewritten as:
using namespace One;
int _tmain()
{
Common c1(3);
Two::Common c2(3);
Do();
Two::Do();
return 0;
}
Modern class libraries are each in their own namespace—for example, the
templates in the Standard Template Library are in the namespace
std
. The developers of the .NET Framework built on this concept,
dividing the class libraries into namespaces and sub-namespaces. This makes them
easier to learn and document.
To use a class in a namespace, you have two choices:
- Call the class by its full name (such as
System::Math
) whenever you're using it:x = System::Math::PI / 4; System::String* s = new System::String("hello");
- Add a
using
statement at the top of the file, and then call the class by its name within the namespace:using namespace System; ... x = Math::PI / 4; String* s = new String("hello");
Punctuation: Using . or :: - In most other .NET languages, the punctuation between the namespace name and the class name is a dot (
.
). For example, in both Visual Basic and C#, a developer would typeSystem.Math.PI
. But in C++, you use a double colon (::
), called the scope-resolution operator. In the documentation, if you see a reference toSystem.Something
, you just need to change it toSystem::Something
in your code. Use the scope-resolution operator between namespace and sub-namespace, namespace and class, or sub-namespace and class.As always, you use the dot between the name of an object and an ordinary member function, and the scope-resolution operator between the name of the class and a static member function or variable. In the previous examples,
PI
is a static member variable of theMath
class. In other .NET languages, the punctuation between class or object name and function is always a dot, even when the function is static. This can make the documentation confusing. Most occurrences of.
in the documentation should be changed to::
.IntelliSense, the feature that pops up lists for you to choose from as you type, really helps with this confusion. If you type "System." into a file of C++ code, no list appears and the status bar reads:
IntelliSense: 'Could not resolve type for expression to the left of . or ->'On the other hand, if you type "System::", a list of namespaces and classes appears for you to choose from. Use the lack of feedback from IntelliSense as an indicator that you have typed the wrong thing, and you'll find working from the documentation a lot less confusing.
The second choice is a better approach when you're going to be typing class
names from the namespace a number of times, because it saves you typing the
namespace name repeatedly. I prefer the first choice when I'm only typing a
class name once, because the fuller name gives more clues to a maintainer about
what the code is doing. This is even more important when you're using classes
from more obscure namespaces—everyone uses classes from System
and
is familiar with many of them, but the System::Web::Security
namespace, for example, might not be so
obvious to you or to those who will maintain your code.
Whether you choose to add a using
statement to your
file or not, you must add a #using
directive to the top
of your source file. When you create a new .NET project, one of these directives
is added for you automatically:
#using <mscorlib.dll>
This gives you access to all the classes that are directly under the
System
namespace, such as the System::Math
class used in these examples. The documentation for the classes that are in
sub-namespaces of System
includes a line like this one, from System::Xml.Document
:
Assembly: System.XML.dll
This is a clue that you need to add this line to your file:
#using <System.XML.dll>
Don't worry about what seem to be extra dots in the filename, and don't
change dots to ::
here. If you will be using a particular
assembly in every file within a project, you can add a reference to the assembly
instead (right-click References in Solution View and choose Add Reference).
The System Namespace
You'll use the classes in the System
namespace in almost every
.NET application. All the data types are represented, for example. Two classes
in particular deserve special mention: System::Console
and System::String
.
The System::Console Class
System::Console
, called System.Console
in
the documentation, represents the screen and keyboard in a simple console
application. After you add a using
statement to your file
so you don't have to type System::
every time, the
Console
class is simple to use. To write a line of text to the
screen, you use the static function WriteLine()
:
Console::WriteLine("Calculations in Progress");
If you want to write text without a following line break, use the
Write()
function instead.
To read a line of text from the keyboard, first you should write out a line
to prompt the users, and then read the entire line into a System::String
object with the static ReadLine()
function:
Console::WriteLine("Enter a sentence:");
String* sentence = Console::ReadLine();
If you want to read in something more complicated than a single string, there
really isn't any support for it within the Console
class, nor the
classes in the System::IO
namespace covered later in this
chapter. You can read it into a string and then use string member functions to
separate it into the pieces you want.
If you want to write formatted output, there's an overload of
WriteLine
that's reminiscent of printf()
, except that
you don't have to tell the function the type of each parameter. For example:
Console::WriteLine("The time is {0} at this moment",
System::DateTime::Now.ToShortTimeString() );
You can write out a number of parameters at once. Use the placeholders, the things in brace brackets in the format string, to call for the parameter you want. As you can see, the count is zero-based. Here's an example:
Console::WriteLine("{0} lives at {1}", name, address);
The System::String and System::Stringbuilder Classes
The String
class represents a string, such as "Hello" or "Kate
Gregory". That's familiar ground for any programmer. But working with .NET
strings can be quite strange for an experienced C++ or MFC programmer. They are
certainly very far removed from the arrays of characters that you might be used
to working with.
If you've ever worked with the MFC class CString
, you've
probably written code like this:
CString message = "Value of x, ";
message += x;
message += "is over limit.";
You might guess that the .NET equivalent would be:
String* message = "Value of x, ";
message += x;
message += "is over limit.";
This just gets you a lot of strange compiler errors about illegal pointer
arithmetic. The message variable is a pointer to a String
instance,
so you can't use the +
operator. You can't do it in a single line
either, like this:
String* message = "Value of x, " + x + "is over limit.";
The bottom line is that you can't treat .NET strings like C++ or C strings.
So how do you build a string from several substrings, or from several pieces in
general? If you want to build it so you can write it out, forget building the
string, and use formatted output as described in the previous section. Or use
the Format()
method, which is reminiscent of
sprintf()
, and of the Format()
method of the old MFC
class CString
. But if you need to build a string in little bits and
pieces, your best choice is a companion class called StringBuilder
,
from the System::Text
namespace. You use a string builder
like this:
String* name = "Kate";
System::Text::StringBuilder* sb = new System::Text::StringBuilder("Hello ");
sb->Append(name);
Using a string builder is more efficient than modifying a string as you go,
because .NET strings actually can't be modified; instead, a whole new one is
created with your changes, and the old one is cleaned up later.
StringBuilder
has all sorts of useful methods like
Append()
, Insert()
, Remove()
, and
Replace()
that you can use to work on your string. When it's ready,
just pass the string builder object to anything that's expecting a string:
Console::WriteLine(sb);
The framework gets the built string from the string builder and passes it to the function for you.
The String
class has its own useful methods too. Consider the
problem mentioned earlier—reading something other than a single string from the
keyboard. The easiest way for you to tackle the problem is to write code that
reads the line of input into a string, and then works with it. Here's a simple
example:
Console::WriteLine("Enter three integers:");
String* input = Console::ReadLine();
String* numbers[] = input->Split(0);
int a1 = Convert::ToInt32(numbers[0]);
int a2 = Convert::ToInt32(numbers[1]);
int a3 = Convert::ToInt32(numbers[2]);
This code uses the Split()
member function of the
String
class. It splits a String
into an array of
strings based on a separator character. If you pass in a null pointer (0), as in
this example, it splits the string based on whitespace such as spaces or tabs,
which is perfect for this situation. The ToInt32()
method of the
Convert
class converts a String
to an integer.
If you already know how to manipulate strings, you might appreciate a quick
"cheat sheet" for the String
class. Table 3.1 is just such a
summary.
Table 3.1 String Functions
C Runtime | MFC CString | System::String |
strcpy |
operator= |
operator= |
strcat |
operator+= |
Append |
strchr |
Find |
IndexOf |
strcmp |
operator == or Compare |
Compare |
strlen |
GetLength() |
Length |
strtok |
n/a | Split |
[] |
[] or GetAt() |
Chars |
sprintf |
Format |
Format |
n/a | Left or Right or Mid |
Substring |
If you've worked with strings in other languages, you'll appreciate System::String
functions such as PadLeft()
,
PadRight()
, Remove()
, and StartsWith()
.
If those names aren't familiar to you, check the Visual C++ documentation. You
might be able to do what you want with a single function call!
The System::DateTime Structure
The DateTime
structure represents a date or a time, or both. It
has a number of useful constructors to create instances using a numeric date and
time, or a number of ticks (useful when you're working with older C++ code).
Here are some examples:
DateTime defaultdate;
Console::WriteLine(defaultdate.ToLongDateString());
DateTime Sept28(2003,9,28,14,30,0,0);
Console::Write(Sept28.ToShortDateString());
Console::Write(S" ");
Console::WriteLine(Sept28.ToShortTimeString());
DateTime now = DateTime::Now;
Console::WriteLine(now.ToString());
It can be intimidating to remember the parameters to the seven-integer constructor, but it's simple when you realize they go from largest to smallest: year, month, day, hour, minute, second, and millisecond. The millisecond parameter is optional and if you want, you can omit all the time parameters completely.
On September 17, 2003, this code produces the following output:
Monday, January 01, 0001
9/28/2003 2:30 PM
9/17/2003 1:17:41 PM
It matters that DateTime
is a structure, not a class, because it
is managed data. Managed classes can only be allocated on the heap with new
; managed structures can only be allocated on the stack as
in these examples.
To get the individual parts of a date, use these properties:
Day
: The day of the monthMonth
: 1 to 12Year
: Always four digitsHour
: 0 to 23Minute
Second
DayOfWeek
: 0 means SundayFormat
: Creates a string based on the time and date, using a format string
The format string passed to Format()
is either a single
character representing one of a number of "canned" formats, or a custom format
string. The most useful canned formats include:
d
: A short date, such as 12/19/00D
: A long date, such as Tuesday, December 19, 2000f
: A full time and date, such as Tuesday, December 19, 2000 17:49g
: A general time and date, such as 12/19/00 17:49s
: A sortable time and date, such as 2000-12-19 17:49:03t
: A short time, such as 17:49T
: A long time, such as 17:49:03
If none of the canned formats has what you need, you can make your own by passing in strings such as "MMMM d, yy" for "December 3, 02" or whatever else you desire. You can find all the format strings in the help installed with Visual Studio.
Other Useful Namespaces
There are plenty of other useful classes contained in sub-namespaces of the
System
namespace. Some are covered elsewhere in this book. Four are
covered here because almost every .NET developer is likely to use them: System::IO
, System::Text
, System::Collections
, and System::Threading
.
The System::IO Namespace
Getting information from users and providing it to them is the sort of task
that can be incredibly simple (like reading a string and echoing it back to the
users) or far more complex. The most basic operations are in the
Console
class in the System
namespace. More
complicated tasks are in the System::IO
namespace. This
namespace includes 27 classes, as well as some structures and other related
utilities. They handle tasks such as:
- Reading and writing to a file
- Binary reads and writes (bytes or blocks of bytes)
- Creating, deleting, renaming, or moving files
- Working with directories
This snippet uses the FileInfo
class to determine whether a file
exists, and then deletes it if it does:
System::IO::FileInfo* fi = new System::IO::FileInfo("c:\\test.txt");
if (fi->Exists)
fi->Delete();
This snippet writes a string to a file:
System::IO::StreamWriter* streamW =
new System::IO::StreamWriter("c:\\test.txt");
streamW->Write("Hi there" );
streamW->Close();
Be sure to close all files, readers, and writers when you have finished with
them. The garbage collector might not finalize the streamW
instance
for a long time, and the file stays open until you explicitly close it or until
the instance that opened it is finalized.
When Typing Strings with Backslashes - The backslash character (\) in the filename must be "escaped" by placing another backslash before it. Otherwise the combination \t will be read as a tab character. This is standard C++ behavior when typing strings with backslashes.
Check the documentation to learn more about IO classes that you can use in console applications, Windows applications, and class libraries. Keep in mind also that many classes can persist themselves to and from a file, or to and from a stream of XML.
The System::Text Namespace
Just as Console
offers simple input and output abilities, the
simplest string work can be tackled with just the String
class from
the System
namespace. More complicated work involves the System::Text
namespace. You've already seen System::Text::StringBuilder
. Other classes in this namespace
handle conversions between different types of text, such as Unicode and
ASCII.
The System::Text::RegularExpressions
namespace lets
you use regular expressions in string manipulations and elsewhere. Here is a
function that determines whether a string passed to it is a valid US ZIP
code:
using namespace System;
using namespace System::Text::RegularExpressions;
// . . .
String* Check(String* code)
{
String* error = S"OK";
Match* m;
switch (code->get_Length())
{
case 5:
Regex* fivenums;
fivenums = new Regex("\\d\\d\\d\\d\\d");
m = fivenums->Match(code);
if (!m->Success)
error = S"Non numeric characters in 5 digit code";
break;
case 10:
Regex* fivedashfour;
fivedashfour = new Regex("\\d\\d\\d\\d\\d-\\d\\d\\d\\d");
m = fivedashfour->Match(code);
if (!m->Success)
error = S"Not a valid zip+4 code";
break;
default:
error = S"invalid length";
}
return error;
}
The Regex
class represents a pattern, such as "five numbers" or
"three letters." The Match
class represents a possible match
between a particular string and a particular pattern. This code checks the
string against two patterns representing the two sets of rules for ZIP
codes.
The syntax for regular expressions in the .NET class libraries will be
familiar to developers who have used regular expression as MFC programmers, or
even as UNIX users. In addition to using regular expressions with classes from
the System::Text
namespace, you can use them in the Find
and Replace dialog boxes of the Visual Studio editor, and with ASP.NET
validation controls. It's worth learning how they work.
Regular Expression Syntax
A regular expression is some text combined with special characters that represent things that can't be typed, such as "the end of a string" or "any number" or "three capital letters."
When regular expressions are being used, some characters give up their usual meaning and instead stand in for one or more other characters. Regular expressions in Visual C++ are built from ordinary characters mixed in with these special entries, shown in Table 3.2.
Here are some examples of regular expressions:
- ^test$ matches only test alone in a string.
- doc[1234] matches doc1, doc2, doc3, or doc4 but not doc5.
- doc[1-4] matches the same strings as doc[1234] but requires less typing.
- doc[^56] matches doca, doc1, and anything else that starts with doc, except doc5 and doc6.n -H\~ello matches Hillo and Hxllo (and lots more) but not Hello. H[^e]llo has the same effect.
- [xy]z matches xz and yz.
- New *York matches New York, NewYork, and New York (with several spaces between the words).
- New +York matches New York and New York, but not NewYork.
- New.*k matches Newk, Newark, and New York, plus lots more.
- World$ matches World at the end of a string, but World\$ matches only World$ anywhere in a string.
Table 3.2 Regular Expression Entries
Entry | Matches |
^ | Start of the string. |
$ | End of the string. |
. | Any single character. |
[] | Any one of the characters within the brackets (use – for a range, ^ for "except"). |
\~ | Anything except the character that follows. |
* | Zero or more of the next character. |
+ | One or more of the next character. |
\w | A single letter or number, or an underscore. (These are called word characters and are the only characters that can be used in a variable name.) |
\s | Whitespace (tabs or spaces). |
\d | A single numerical digit. |
\ | Removes the special meaning from the character that follows. |
The System::Collections Namespace
Another incredibly common programming task is holding on to a collection of
objects. If you have just a few, you can use an array to read three integers in
one line of input. In fact, arrays in .NET are actually objects, instances of
the System::Array
class, which have some useful member
functions of their own, such as Copy()
. There are times when you
want specific types of collections, though, and the System::Collections
namespace has plenty of them. The provided
collections include:
Stack
. A collection that stores objects in order. The object stored most recently is the first taken out.Queue
. A collection that stores objects in order. The first stored is the first taken out.Hashtable
. A collection that can be searched far more quickly than other types of collections, but takes up more space.ArrayList
. An array that grows as elements are added to it.SortedList
. A collection of two-part (key and value) items that can be accessed by key or in numerical order.BitArray
. A compact way to store an array of true/false flags.
One rather striking omission here is a linked list. You have to code your own if you need a linked or double-linked list.
The System::Threading Namespace
Threading has been a difficult part of Windows programming from the very
beginning. It's quite a bit simpler in .NET. The vital classes for threading are
in the System::Threading
namespace. These include classes
such as Mutex
, Thread
, and ThreadPool
,
which developers with experience in threaded applications will recognize
instantly.
A thread is a path of execution through a program. In a multithreaded program, each thread has its own stack and operates independently of other threads running within the same program.
How Many Threads? - Any application always has at least one thread, which is the program's primary or main thread. You can start and stop as many additional threads as you need, but the main thread keeps running as long as the application is active.
A thread is the smallest unit of execution, much smaller than a process. Generally, each running application on your system is a process. If you start the same application twice (for example, Notepad), there are two processes: one for each instance. It is possible for several instances of an application to share a single process: For example, if you choose File, New Window in Internet Explorer, two applications appear on your taskbar, and they share a process. The unfortunate consequence is that if one instance crashes, they all do.
Writing a multithreaded application is simple with classes from the System::Thread
namespace. First, you need to think of some work
for a new thread to do: some slow calculation that you want to run without
slowing the responsiveness of your application. This work will go into a
function, and the function will be executed on a new thread.
The Thread Proc - For a long time, Windows C++ programmers have called the function the thread proc (short for procedure).
Your thread proc must be a member function of a garbage-collected class. For example:
public __gc class Counter
{
public:
void Countdown()
{
String* threadName = Thread::CurrentThread->Name;
Console::Write(S"This is the current thread: ");
Console::WriteLine(threadName);
for (int counter = maxCount; counter >= 1; counter--)
{
Console::WriteLine(S"{0} is currently on {1}.",threadName,
__box(counter));
}
Console::WriteLine(S"{0} has finished counting down from {1}.",
threadName,
__box(maxCount));
}
};
The heart of this function is the for
loop that counts
down from maxCount
to zero. It uses the Name
property
of the thread that is executing the function, because the sample that uses this
class calls the function on two different threads. Normally, the function that
does the asynchronous work would not communicate with the user; it would be
quietly working in the background, doing some long slow work. It might, however,
update a progress bar, or an icon that indicates a background task is in
progress.
This code calls the thread proc on a new thread and also on the application's main thread:
Console::Write("Enter a number to count down from: ");
maxCount = Int16::Parse(Console::ReadLine());
Thread::CurrentThread->Name = "Main Thread";
Counter* counter = new Counter();
ThreadStart* SecondStart = new ThreadStart(counter,Counter::Countdown);
Thread* secondThread = new Thread(SecondStart);
secondThread->Name = "Secondary Thread";
secondThread->Start();
counter->Countdown();
This code keeps the number entered by the user in a global variable called
maxCount
. Because ReadLine
returns a pointer to a
System::String
instance, the static Parse()
method of Int16
is used to convert a string to an integer so that
it can be stored in maxCount
.
To execute a particular function on a new thread, you create a
Thread
object and call its Start()
method. To create a
Thread
object, you need a ThreadStart
object, and the
constructor for ThreadStart
needs a reference to an instance of a
garbage-collected class (counter
in this example), and a function
pointer (just the name of the function without the parentheses) to a member
function of that garbage-collected class.
Because this code calls Countdown
on a new thread and also on
the main thread, the output shows the two threads taking turns, as in Figure
3.1.
Figure 3.1
A multithreaded application demonstrates how threads take turns doing their work.
In Brief
- The libraries that come with the .NET Framework are large, and cover almost every task programmers are likely to tackle. Using them can save programmers hours or days of work.
- The same class libraries are used in Visual Basic, C#, and managed C++, but managed C++ can also call some unmanaged libraries that Visual Basic and C# cannot access.
- The
System
namespace holds classes for simple common tasks, and a large number of sub-namespaces for slightly less common (but still important) tasks, including string manipulation, IO, and threading.