Ivor Horton’s
Beginning Visual C++® 2005 Ivor Horton
Ivor Horton’s
Beginning Visual C++® 2005
Ivor Horton’s
Beginning Visual C++® 2005 Ivor Horton
Ivor Horton’s Beginning Visual C++® 2005 Published by Wiley Publishing, Inc. 10475 Crosspoint Boulevard Indianapolis, IN 46256 www.wiley.com Copyright © 2006 by Ivor Horton Published by Wiley Publishing, Inc., Indianapolis, Indiana Published simultaneously in Canada ISBN-13: 978-0-7645-7197-8 ISBN-10: 0-7645-7197-4 Manufactured in the United States of America 10 9 8 7 6 5 4 3 2 1 1B/QY/QS/QW/IN Library of Congress Cataloging-in-Publication Data: Horton, Ivor. Ivor Horton’s Beginning Visual C++ 2005 / Ivor Horton. p. cm. Includes index. ISBN-13: 978-0-7645-7197-8 (paper/website) ISBN-10: 0-7645-7197-4 (paper/website) 1. C++ (Computer program language) 2. Microsoft Visual C++. I. Title: Beginning Visual C++ 2005. II. Title. QA76.73.C15I6694 2006 005.13’3—dc22 2005032051 No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Legal Department, Wiley Publishing, Inc., 10475 Crosspoint Blvd., Indianapolis, IN 46256, (317) 572-3447, fax (317) 572-4355, or online at http://www.wiley.com/go/permissions. LIMIT OF LIABILITY/DISCLAIMER OF WARRANTY: THE PUBLISHER AND THE AUTHOR MAKE NO REPRESENTATIONS OR WARRANTIES WITH RESPECT TO THE ACCURACY OR COMPLETENESS OF THE CONTENTS OF THIS WORK AND SPECIFICALLY DISCLAIM ALL WARRANTIES, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE. NO WARRANTY MAY BE CREATED OR EXTENDED BY SALES OR PROMOTIONAL MATERIALS. THE ADVICE AND STRATEGIES CONTAINED HEREIN MAY NOT BE SUITABLE FOR EVERY SITUATION. THIS WORK IS SOLD WITH THE UNDERSTANDING THAT THE PUBLISHER IS NOT ENGAGED IN RENDERING LEGAL, ACCOUNTING, OR OTHER PROFESSIONAL SERVICES. IF PROFESSIONAL ASSISTANCE IS REQUIRED, THE SERVICES OF A COMPETENT PROFESSIONAL PERSON SHOULD BE SOUGHT. NEITHER THE PUBLISHER NOR THE AUTHOR SHALL BE LIABLE FOR DAMAGES ARISING HEREFROM. THE FACT THAT AN ORGANIZATION OR WEBSITE IS REFERRED TO IN THIS WORK AS A CITATION AND/OR A POTENTIAL SOURCE OF FURTHER INFORMATION DOES NOT MEAN THAT THE AUTHOR OR THE PUBLISHER ENDORSES THE INFORMATION THE ORGANIZATION OR WEBSITE MAY PROVIDE OR RECOMMENDATIONS IT MAY MAKE. FURTHER, READERS SHOULD BE AWARE THAT INTERNET WEBSITES LISTED IN THIS WORK MAY HAVE CHANGED OR DISAPPEARED BETWEEN WHEN THIS WORK WAS WRITTEN AND WHEN IT IS READ. For general information on our other products and services please contact our Customer Care Department within the United States at (800) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002. Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in the United States and other countries, and may not be used without written permission. Visual C++ is a registered trademark of Microsoft Corporation in the United States and/or other countries. All other trademarks are the property of their respective owners. Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book. Wiley also publishes its books in a variety of electronic formats. Some content that appears in print may not be available in electronic books.
Credits Executive Editor
Vice President and Executive Publisher
Bob Elliott
Joseph B. Wikert
Senior Development Editor
Project Coordinator
Kevin Kent
Development Editor Howard Jones
Technical Editor John Mueller
Production Editor Pamela Hanley
Copy Editor Susan Hobbs
Editorial Manager Mary Beth Wakefield
Production Manager Tim Tate
Ryan Steffen
Graphics and Production Specialists Brian Drumm Lauren Goddard Brooke Graczyk Denny Hager Joyce Haughey Jennifer Heleine Stephanie D. Jumper Barry Offringa Alicia South Erin Zeltner
Quality Control Technicians Laura Albert John Greenough Leeann Harney
Proofreading TECHBOOKS Production Services
Vice President and Executive Group Publisher
Indexing
Richard Swadley
Infodex Indexing Services Inc.
About the Author Ivor Horton graduated as a mathematician and was lured into information technology by promises of great rewards for very little work. In spite of the reality being usually a great deal of work for relatively modest rewards, he has continued to work with computers to the present day. He has been engaged at various times in programming, systems design, consultancy, and the management of the implementation of projects of considerable complexity. Horton has many years of experience in the design and implementation of computer systems applied to engineering design and to manufacturing operations in a variety of industries. He also has considerable experience in developing occasionally useful applications in a wide variety of programming languages, and of teaching primarily scientists and engineers to do likewise. He has written books on programming for more than 10 years; his currently published works include tutorials on C, C++, and Java. At the present time, when he is not writing programming books or providing advice to others, he spends his time fishing, travelling, and trying to speak better French.
This book is dedicated to Alexander Gilbey. I look forward to his comments, but I’ll probably have to wait a while.
Acknowledgments I’d like to acknowledge the efforts and support of the John Wiley & Sons, and Wrox Press editorial and production team in the production of this book, especially Senior Development Editor Kevin Kent who has been there from way back at the beginning and has stayed through to the end. I’d also like to thank Technical Editor John Mueller for going through the text to find hopefully most of my mistakes, for checking out all the examples in the book, and for his many constructive comments that helped make the book a better tutorial. Finally, I would like to thank my wife, Eve, for her patience, cheerfulness, and support throughout the long gestation period of this book. As I have said on many previous occasions, I could not have done it without her.
Contents Acknowledgments
Chapter 1: Programming with Visual C++ 2005 The .NET Framework The Common Language Runtime (CLR) Writing C++ Applications Learning Windows Programming Learning C++ The C++ Standards Console Applications Windows Programming Concepts
What Is the Integrated Development Environment? Components of the System The The The The
Editor Compiler Linker Libraries
Using the IDE Toolbar Options Dockable Toolbars Documentation Projects and Solutions
xi
1 2 2 3 4 5 5 6 6
8 9 9 9 9 9
10 10 12 12 13
Defining a Project Debug and Release Versions of Your Program Dealing with Errors
13 19 23
Setting Options in Visual C++ 2005 Creating and Executing Windows Applications
27 28
Creating an MFC Application Building and Executing the MFC Application
28 30
Creating a Windows Forms Application
Summary
31
35
xiii
Contents Chapter 2: Data, Variables, and Calculations The Structure of a C++ Program Program Comments The #include Directive — Header Files Namespaces and the Using Declaration
The main() Function Program Statements Whitespace Statement Blocks Automatically Generated Console Programs
37 38 44 45 46
46 47 49 49 50
Defining Variables
51
Naming Variables
51
Keywords in C++
Declaring Variables Initial Values for Variables
52
52 53
Fundamental Data Types
54
Integer Variables Character Data Types Integer Type Modifiers The Boolean Type Floating-Point Types
54 55 56 57 57
Fundamental Types in ISO/ANSI C++
Literals Defining Synonyms for Data Types Variables with Specific Sets of Values Specifying the Type for Enumeration Constants
58
59 60 60 62
Basic Input/Output Operations
62
Input from the Keyboard Output to the Command Line Formatting the Output Escape Sequences
62 63 64 65
Calculating in C++ The Assignment Statement Understanding Lvalues and Rvalues
Arithmetic Operations
xiv
67 67 68
68
The const Modifier Constant Expressions Program Input Calculating the Result Displaying the Result
70 70 71 71 73
Calculating a Remainder
73
Contents Modifying a Variable The Increment and Decrement Operators The Sequence of Calculation
73 74 77
Operator Precedence
77
Variable Types and Casting
78
Rules for Casting Operands Casts in Assignment Statements Explicit Casts Old-Style Casts The Bitwise Operators The The The The The
Bitwise Bitwise Bitwise Bitwise Bitwise
AND OR Exclusive OR NOT Shift Operators
Understanding Storage Duration and Scope Automatic Variables Positioning Variable Declarations Global Variables Static Variables
Namespaces Declaring a Namespace Multiple Namespaces
C++/CLI Programming
78 79 80 81 81 82 84 85 86 86
88 88 91 91 94
95 96 98
99
C++/CLI Specific: Fundamental Data Types C++/CLI Output to the Command Line C++/CLI Specific — Formatting the Output C++/CLI Input from the Keyboard Using safe_cast C++/CLI Enumerations
99 104 104 107 108 109
Specifying a Type for Enumeration Constants Specifying Values for Enumeration Constants
111 111
Summary Exercises
Chapter 3: Decisions and Loops Comparing Values The if Statement Nested if Statements The Extended if Statement Nested if-else Statements
112 113
115 115 117 118 120 122
xv
Contents Logical Operators and Expressions Logical AND Logical OR Logical NOT
The Conditional Operator The switch Statement Unconditional Branching
Repeating a Block of Statements What Is a Loop? Variations on the for Loop Using the continue Statement Floating-Point Loop Counters
The while Loop The do-while Loop Nested Loops
C++/CLI Programming The for each Loop
Summary Exercises
Chapter 4: Arrays, Strings, and Pointers Handling Multiple Data Values of the Same Type Arrays Declaring Arrays Initializing Arrays Character Arrays and String Handling String Input
Multidimensional Arrays Initializing Multidimensional Arrays
124 125 125 126
127 129 132
132 132 135 139 143
143 146 147
150 153
156 157
159 160 160 161 164 166 167
169 170
Indirect Data Access
172
What Is a Pointer? Declaring Pointers
172 173
The Address-Of Operator
Using Pointers The Indirection Operator Why Use Pointers?
Initializing Pointers Pointers to char
The sizeof Operator Constant Pointers and Pointers to Constants
xvi
173
174 174 174
176 177
181 183
Contents Pointers and Arrays Pointer Arithmetic Using Pointers with Multidimensional Arrays Pointer Notation with Multidimensional Arrays
Dynamic Memory Allocation The Free Store, Alias the Heap The new and delete Operators Allocating Memory Dynamically for Arrays Dynamic Allocation of Multidimensional Arrays
Using References What Is a Reference? Declaring and Initializing References
C++/CLI Programming Tracking Handles Declaring Tracking Handles
CLR Arrays Sorting One-Dimensional Arrays Searching One-Dimensional Arrays Multidimensional Arrays Arrays of Arrays
Strings Joining Strings Modifying Strings Searching Strings
Tracking References Interior Pointers
Summary Exercises
Chapter 5: Introducing Structure into Your Programs Understanding Functions Why Do You Need Functions? Structure of a Function The Function Header The Function Body The return Statement
Using a Function Function Prototypes
Passing Arguments to a Function The Pass-by-value Mechanism Pointers as Arguments to a Function
185 185 190 191
192 192 193 194 196
197 197 197
198 199 199
200 205 206 209 213
216 217 220 222
225 225
228 230
231 232 233 233 233 234 235
235 235
239 240 241
xvii
Contents Passing Arrays to a Function Passing Multi-Dimensional Arrays to a Function
References as Arguments to a Function Use of the const Modifier Arguments to main() Accepting a Variable Number of Function Arguments
Returning Values from a Function Returning a Pointer A Cast Iron Rule for Returning Addresses
Returning a Reference A Teflon-Coated Rule: Returning References
Static Variables in a Function
Recursive Function Calls Using Recursion
C++/CLI Programming Functions Accepting a Variable Number of Arguments Arguments to main()
Summary Exercises
Chapter 6: More about Program Structure Pointers to Functions Declaring Pointers to Functions A Pointer to a Function as an Argument Arrays of Pointers to Functions
243 245
247 249 250 252
254 254 256
258 260
260
262 264
265 266 267
268 269
271 271 272 275 277
Initializing Function Parameters Exceptions
277 279
Throwing Exceptions Catching Exceptions Exception Handling in the MFC
281 282 283
Handling Memory Allocation Errors Function Overloading What Is Function Overloading? When to Overload Functions
Function Templates
284 285 286 288
288
Using a Function Template
289
An Example Using Functions
291
Implementing a Calculator
291
Analyzing the Problem
292
Eliminating Blanks from a String Evaluating an Expression
xviii
294 295
Contents Getting the Value of a Term Analyzing a Number Putting the Program Together Extending the Program Extracting a Substring Running the Modified Program
C++/CLI Programming Understanding Generic Functions Defining Generic Functions Using Generic Functions
A Calculator Program for the CLR Removing Spaces from the Input String Evaluating an Arithmetic Expression Obtaining the Value of a Term Evaluating a Number Extracting a Parenthesized Substring
Summary Exercises
Chapter 7: Defining Your Own Data Types The struct in C++ What Is a struct? Defining a struct Initializing a struct Accessing the Members of a struct Intellisense Assistance with Structures The struct RECT Using Pointers with a struct Accessing Structure Members through a Pointer The Indirect Member Selection Operator
Data Types, Objects, Classes and Instances
298 299 302 304 305 307
308 309 309 310
315 316 316 318 318 319
320 321
323 324 324 324 325 325 329 330 330 332 332
332
First Class Operations on Classes Terminology
334 334 335
Understanding Classes
335
Defining a Class Access Control in a Class
Declaring Objects of a Class Accessing the Data Members of a Class Member Functions of a Class
336 336
336 337 339
xix
Contents Positioning a Member Function Definition Inline Functions
Class Constructors What Is a Constructor? The Default Constructor Assigning Default Parameter Values in a Class Using an Initialization List in a Constructor
Private Members of a Class Accessing private Class Members The friend Functions of a Class Placing friend Function Definitions Inside the Class
The Default Copy Constructor
The Pointer this const Objects of a Class const Member Functions of a Class Member Function Definitions Outside the Class
Arrays of Objects of a Class Static Members of a Class Static Data Members of a Class Static Function Members of a Class
Pointers and References to Class Objects Pointers to Class Objects References to Class Objects Implementing a Copy Constructor
C++/CLI Programming Defining Value Class Types The ToString() Function in a Class Literal Fields
Defining Reference Class Types Class Properties Defining Scalar Properties Trivial Scalar Properties Defining Indexed Properties More Complex Indexed Properties Static Properties Reserved Property Names
initonly Fields Static Constructors
Summary Exercises
xx
341 342
343 343 345 347 350
350 353 354 356
356
358 360 361 362
363 364 365 367
368 368 371 371
372 373 376 377
378 381 382 384 388 392 393 394
394 396
396 397
Contents Chapter 8: More on Classes Class Destructors What Is a Destructor? The Default Destructor Destructors and Dynamic Memory Allocation
399 399 399 400 402
Implementing a Copy Constructor Sharing Memory Between Variables
405 407
Defining Unions Anonymous Unions Unions in Classes and Structures
408 409 410
Operator Overloading Implementing an Overloaded Operator Implementing Full Support for an Operator Overloading the Assignment Operator Fixing the Problem
Overloading the Addition Operator Overloading the Increment and Decrement Operators
Class Templates Defining a Class Template Template Member Functions
Creating Objects from a Class Template Class Templates with Multiple Parameters
Using Classes The Idea of a Class Interface Defining the Problem Implementing the CBox Class Comparing CBox Objects Combining CBox Objects Analyzing CBox Objects
Defining the CBox Class Adding Data Members Defining the Constructor Adding Function Members Adding Global Functions
Using Our CBox Class
Organizing Your Program Code Naming Program Files
C++/CLI Programming Overloading Operators in Value Classes Overloading the Increment and Decrement Operators Overloading Operators in Reference Classes
Summary Exercises
410 411 414 418 418
423 426
427 428 430
431 434
436 436 436 437 438 439 441
445 446 447 448 453
455
458 460
461 461 467 467
470 471
xxi
Contents Chapter 9: Class Inheritance and Virtual Functions Basic Ideas of OOP Inheritance in Classes
473 473 475
What Is a Base Class? Deriving Classes from a Base Class
475 476
Access Control Under Inheritance
479
Constructor Operation in a Derived Class Declaring Class Members to be Protected The Access Level of Inherited Class Members
The Copy Constructor in a Derived Class Class Members as Friends Friend Classes Limitations on Class Friendship
Virtual Functions What Is a Virtual Function? Using Pointers to Class Objects Using References With Virtual Functions Incomplete Class Definitions
Pure Virtual Functions Abstract Classes Indirect Base Classes Virtual Destructors
Casting Between Class Types Nested Classes C++/CLI Programming Inheritance in C++/CLI Classes Interface Classes Defining Interface Classes Classes and Assemblies Visibility Specifiers for Classes and Interfaces Access Specifiers for Class and Interface Members
Functions Specified as new Delegates and Events Declaring Delegates Creating Delegates Unbound Delegates Creating Events
Destructors and Finalizers in Reference Classes Generic Classes Generic Interface Classes Generic Collection Classes
Summary Exercises
xxii
482 486 489
490 495 496 496
497 499 501 503 504
505 505 508 511
516 516 520 520 526 527 531 531 531
536 536 537 537 541 545
549 551 554 555
561 562
Contents Chapter 10: Debugging Techniques Understanding Debugging Program Bugs Common Bugs
Basic Debugging Operations Setting Breakpoints Advanced Breakpoints
Setting Tracepoints Starting Debugging Inspecting Variable Values Viewing Variables in the Edit Window
Changing the Value of a Variable
Adding Debugging Code Using Assertions Adding Your Own Debugging Code
565 565 567 567
568 570 572
572 573 576 577
577
578 578 580
Debugging a Program
585
The Call Stack Step Over to the Error
585 587
Testing the Extended Class Finding the Next Bug
Debugging Dynamic Memory Functions Checking the Free Store Controlling Free Store Debug Operations Free Store Debugging Output
Debugging C++/CLI Programs Using the Debug and Trace Classes Generating Output Setting the Output Destination Indenting the Output Controlling Output Assertions
Summary
Chapter 11: Windows Programming Concepts Windows Programming Basics Elements of a Window Windows Programs and the Operating System Event-Driven Programs Windows Messages The Windows API Windows Data Types Notation in Windows Programs
591 593
593 594 595 596
602 602 603 604 604 605 607
611
613 614 614 616 617 617 617 618 619
xxiii
Contents The Structure of a Windows Program The WinMain() Function Specifying a Program Window Creating a Program Window Initializing the Program Window Dealing with Windows Messages A Complete WinMain() Function
Message Processing Functions The WindowProc() Function Decoding a Windows Message Ending the Program A Complete WindowProc() Function
A Simple Windows Program
620 621 623 625 627 627 631
632 633 633 636 636
637
Windows Program Organization The Microsoft Foundation Classes
638 640
MFC Notation How an MFC Program Is Structured
640 640
The Application Class The Window Class Completing the Program The Finished Product
Using Windows Forms Summary
Chapter 12: Windows Programming with the Microsoft Foundation Classes The Document/View Concept in MFC What Is a Document? Document Interfaces What Is a View? Linking a Document and Its Views Document Templates Document Template Classes
641 642 642 643
645 647
649 650 650 650 651 652 652 653
Your Application and MFC
653
Creating MFC Applications
655
Creating an SDI Application The Output from the MFC Application Wizard Viewing Project Files Viewing Classes The Class Definitions Creating an Executable Module
xxiv
657 660 662 663 664 667
Contents Running the Program How the Program Works
668 669
Creating an MDI Application
671
Running the Program
Summary Exercises
Chapter 13: Working with Menus and Toolbars Communicating with Windows
672
674 674
677 677
Understanding Message Maps
678
Message Handler Definitions
679
Message Categories Handling Messages in Your Program How Command Messages Are Processed
Extending the Sketcher Program Elements of a Menu
681 682 682
683 684
Creating and Editing Menu Resources
684
Adding a Menu Item to the Menu Bar Adding Items to the Element Menu Modifying Existing Menu Items Completing the Menu
684 686 687 687
Adding Handlers for Menu Messages Choosing a Class to Handle Menu Messages Creating Menu Message Functions Coding Menu Message Functions Adding Members to Store Color and Element Mode Initializing the New Class Data Members Running the Extended Example
Adding Message Handlers to Update the User Interface Coding a Command Update Handler Exercising the Update Handlers
Adding Toolbar Buttons Editing Toolbar Button Properties Exercising the Toolbar Buttons Adding Tooltips
Summary Exercises
688 690 690 692 692 694 696
697 697 699
700 701 703 703
704 705
xxv
Contents Chapter 14: Drawing in a Window
707
Basics of Drawing in a Window
707
The Window Client Area The Windows Graphical Device Interface What Is a Device Context? Mapping Modes
The Drawing Mechanism in Visual C++ The View Class in Your Application The OnDraw() Member Function
The CDC Class Displaying Graphics Drawing in Color
708 709 709 709
711 711 711
712 713 717
Drawing Graphics in Practice Programming the Mouse
721 725
Messages from the Mouse
725
WM_LBUTTONDOWN WM_MOUSEMOVE WM_LBUTTONUP
Mouse Message Handlers Drawing Using the Mouse Getting the Client Area Redrawn Defining Classes for Elements The CElement Class The CLine Class Calculating the Enclosing Rectangle for a Line The CRectangle Class The CCircle Class The CCurve Class Completing the Mouse Message Handlers
Exercising Sketcher Running the Example Capturing Mouse Messages
Summary Exercises
Chapter 15: Creating the Document and Improving the View What Are Collection Classes? Types of Collection The Type-Safe Collection Classes
xxvi
726 726 727
727 729 731 732 736 737 742 742 744 746 747
753 754 755
756 757
759 759 760 761
Contents Collections of Objects
761
The CArray Template Class Helper Functions The CList Template Class The CMap Template Class
761 763 763 768
The Typed Pointer Collections
771
The CTypedPtrList Template Class CTypePtrList Operations
771 771
Using the CList Template Class
773
Drawing a Curve Defining the CCurve Class Implementing the CCurve Class Exercising the CCurve Class
Creating the Document Using a CTypedPtrList Template Implementing the Document Destructor Drawing the Document Adding an Element to the Document Exercising the Document
Improving the View Updating Multiple Views Scrolling Views Logical Coordinates and Client Coordinates Dealing with Client Coordinates
Using MM_LOENGLISH Mapping Mode
Deleting and Moving Shapes Implementing a Context Menu Associating a Menu with a Class Choosing a Context Menu Identifying a Selected Element Exercising the Pop-Ups Checking the Context Menu Items
Highlighting Elements Drawing Highlighted Elements Exercising the Highlights
Servicing the Menu Messages Deleting an Element Moving an Element Getting the Elements to Move Themselves Exercising the Application
Dealing with Masked Elements Summary Exercises
774 775 777 778
779 779 780 781 783 784
785 785 787 789 790
792
793 794 795 797 798 800 800
802 806 807
807 807 808 811 814
814 815 816
xxvii
Contents Using a List Box Removing the Scale Dialog Creating a List Box Control Creating the Dialog Class Displaying the Dialog
Using an Edit Box Control Creating an Edit Box Resource Creating the Dialog Class The CString Class
Adding the Text Menu Item Defining a Text Element Implementing the CText Class The CText Constructor Drawing a CText Object Moving a CText Object
Creating a Text Element
Summary Exercises
Chapter 17: Storing and Printing Documents Understanding Serialization Serializing a Document Serialization in the Document Class Definition Serialization in the Document Class Implementation The Serialize() Function The CArchive Class
Functionality of CObject-Based Classes The Macros Adding Serialization to a Class
How Serialization Works How to Implement Serialization for a Class
Applying Serialization Recording Document Changes Serializing the Document Serializing the Element Classes The Serialize() Functions for the Shape Classes
852 852 853 854 855
856 856 858 858
859 860 861 861 862 862
863
864 865
867 867 868 868 869 870 870
872 872
873 874
874 874 876 877 879
Exercising Serialization Moving Text Printing a Document
881 882 884
The Printing Process
885
The CPrintInfo Class
886
xxix
Contents Chapter 16: Working with Dialogs and Controls Understanding Dialogs Understanding Controls Common Controls
Creating a Dialog Resource Adding Controls to a Dialog Box Testing the Dialog
Programming for a Dialog
817 817 818 820
820 820 822
822
Adding a Dialog Class Modal and Modeless Dialogs Displaying a Dialog
823 824 824
Code to Display the Dialog Code to Close the Dialog
826 827
Supporting the Dialog Controls
828
Initializing the Controls Handling Radio Button Messages
Completing Dialog Operations Adding Pen Widths to the Document Adding Pen Widths to the Elements Creating Elements in the View Exercising the Dialog
Using a Spin Button Control Adding the Scale Menu Item and Toolbar Button Creating the Spin Button The Controls’ Tab Sequence
Generating the Scale Dialog Class Dialog Data Exchange and Validation Initializing the Dialog
Displaying the Spin Button
Using the Scale Factor Scaleable Mapping Modes Setting the Document Size Setting the Mapping Mode Implementing Scrolling with Scaling Setting Up the Scrollbars
Working with Status Bars
828 830
831 832 832 833 834
835 835 835 838
838 840 840
841
842 842 844 844 846 847
848
Adding a Status Bar to a Frame
848
Defining the Status Bar Parts Updating the Status Bar
850 851
xxviii
Contents Implementing Multipage Printing
888
Getting the Overall Document Size Storing Print Data Preparing to Print Cleaning Up After Printing Preparing the Device Context Printing the Document Getting a Printout of the Document
889 890 891 892 893 894 898
Summary Exercises
Chapter 18: Writing Your Own DLLs Understanding DLLs How DLLs Work Run-Time Dynamic Linking
Contents of a DLL The DLL Interface The DllMain() Function
DLL Varieties MFC Extension DLL Regular DLL — Statically Linked to MFC Regular DLL — Dynamically Linked to MFC
Deciding What to Put in a DLL Writing DLLs Writing and Using an Extension DLL Understanding DllMain() Adding Classes to the Extension DLL Exporting Classes from the Extension DLL Building a DLL Using the Extension DLL in Sketcher Files Required to Use a DLL
Exporting Variables and Functions from a DLL Importing Symbols into a Program Implementing the Export of Symbols from a DLL Using Exported Symbols
Summary Exercises
xxx
898 899
901 901 903 904
906 906 906
906 906 907 907
907 908 908 910 911 912 913 914 915
916 917 917 918
920 920
Contents Chapter 19: Connecting to Data Sources Database Basics A Little SQL Retrieving Data Using SQL Choosing Records
921 921 924 924 925
Joining Tables Using SQL Sorting Records
926 929
Database Support in MFC
929
MFC Classes Supporting ODBC
930
Creating a Database Application
931
Registering an ODBC Database Generating an MFC ODBC Program
931 933
Snapshot versus Dynaset Recordsets
935
Understanding the Program Structure
936
Understanding Recordsets Understanding the Record View Creating the View Dialog Linking the Controls to the Recordset
Exercising the Example
Sorting a Recordset Modifying the Window Caption
Using a Second Recordset Object Adding a Recordset Class Adding a View Class for the Recordset Creating the Dialog Resource Creating the Record View Class
Customizing the Recordset
937 941 943 946
948
948 949
950 950 954 954 955
957
Adding a Filter to the Recordset Defining the Filter Parameter Initializing the Record View
958 958 960
Accessing Multiple Table Views
961
Switching Views Enabling the Switching Operation Handling View Activation
Viewing Orders for a Product
Viewing Customer Details Adding the Customer Recordset Creating the Customer Dialog Resource Creating the Customer View Class Adding a Filter Implementing the Filter Parameter
961 963 965
966
967 967 968 968 970 972
xxxi
Contents Linking the Order Dialog to the Customer Dialog Exercising the Database Viewer
Summary Exercises
Chapter 20: Updating Data Sources Update Operations CRecordset Update Operations Checking that Operations are Legal Record Locking
Transactions CDatabase Transaction Operations
A Simple Update Example
973 976
976 977
979 979 980 981 982
982 983
984
Customizing the Application
986
Managing the Update Process
988
Implementing Update Mode
990
Enabling and Disabling Edit Controls Changing the Button Label Controlling the Visibility of the Cancel Button Disabling the Record Menu Expediting the Update Implementing the Cancel Operation
Adding Rows to a Table The Order Entry Process Creating the Resources Creating the Recordsets Creating the Recordset Views Adding Controls to the Dialog Resources Implementing Dialog Switching Creating an Order ID Storing the New Order ID Creating the New Order ID Initiating ID Creation
Storing the Order Data Setting Dates
Selecting Products for an Order Adding a New Order
Summary Exercises
xxxii
991 992 993 994 996 997
999 1000 1001 1002 1002 1006 1010 1014 1014 1015 1017
1019 1020
1021 1023
1028 1029
Contents Chapter 21: Applications Using Windows Forms Understanding Windows Forms Understanding Windows Forms Applications Modifying the Properties of a Form How the Application Starts
Customizing the Application GUI Adding Controls to a Form Adding Menus Adding Submenus
Adding a Tab Control Using GroupBox Controls Using Button Controls Using the WebBrowser Control Operation of the Winning Application Adding a Context Menu Creating Event Handlers Event Handlers for Menu Items Adding Members to the Form1 Class Handling the Play Menu Event
Handling Events for the Limits Menu Creating a Dialog Box Adding a List to a ListBox Handling the Dialog Button Events Controlling the State of the ListBox Objects Creating the Dialog Object
Using the Dialog Box Validating the Input Handler the Reset Menu Item Event
Adding the Second Dialog
1031 1031 1032 1034 1035
1035 1036 1037 1038
1040 1042 1044 1047 1048 1049 1049 1050 1050 1052
1056 1056 1058 1060 1061 1061
1062 1063 1067
1068
Getting the Data from the Dialog Controls Disabling Input Controls Updating the Limits Menu Item Handlers
1070 1073 1074
Implementing the Help | About Menu Item Handling a Button Click Responding to the Context Menu
1075 1076 1079
The Logic for Dealing with the Choose Menu Item Creating the Dialog Form Developing the Dialog Class Handling the Click Event for the ChooseMenu
Summary Exercises
1080 1080 1081 1083
1086 1087
xxxiii
Contents Chapter 22: Accessing Data Sources in a Windows Forms Application Working with Data Sources Accessing and Displaying Data Using a DataGridView Control Using a DataGridView Control in Unbound Mode Customizing a DataGridView Control Customizing Header Cells Customizing Non-Header Cells Setting Up the Data Setting Up the Control Setting Up the Column Headers Formatting a Column Customizing Alternate Rows
Dynamically Setting Cell Styles
Using Bound Mode The BindingSource Component Using the BindingNavigator Control Binding to Individual Controls Working with Multiple Tables Summary Exercises
1089 1089 1090 1091 1093 1099 1101 1101 1102 1103 1105 1106 1108
1108
1114 1115 1120 1123 1127 1129 1129
Appendix A: C++ Keywords
1131
Appendix B: ASCII Codes
1133
Index
xxxiv
1139
Introduction Welcome to Beginning Visual C++ 2005. With this book you can become an effective C++ programmer. The latest development system from Microsoft, Visual Studio 2005, supports two distinct but closely related flavors of the C++ language; it fully supports the original ISO/ANSI standard C++, and you also get support for a new version of C++ called C++/CLI that was developed by Microsoft but is now an ECMA standard. These two versions of C++ are complementary and fulfill different roles. ISO/ANSI C++ is there for the development of high-performance applications that run natively on your computer whereas C++/CLI has been developed specifically for the .NET Framework. This book will teach you the essentials of programming applications in both versions of C++. You get quite a lot of assistance from automatically generated code when writing ISO/ANSI C++ programs, but you still need to write a lot of C++ yourself. You need a solid understanding of object-oriented programming techniques, as well as a good appreciation of what’s involved in programming for Windows. Although C++/CLI targets the .NET Framework, it also is the vehicle for the development of Windows Forms applications that you can develop with little or in some cases no, explicit code writing. Of course, when you do have to add code to a Windows Forms application, even though it may be a very small proportion of the total, you still need an in-depth knowledge of the C++/CLI language. ISO/ANSI C++ remains the language of choice for many professionals, but the speed of development that C++/CLI and Windows Forms applications bring to the table make that essential, too. For this reason I decided to cover the essentials of both flavors of C++ in this book.
Who This Book Is For This book is aimed at teaching you how to write C++ applications for the Microsoft Windows operating system using Visual C++ 2005 or any edition of Visual Studio 2005. I make no assumptions about prior knowledge of any particular programming language. This tutorial is for you if: ❑
You have a little experience of programming in some other language, such as BASIC or Pascal, for example, and you are keen to learn C++ and develop practical Microsoft Windows programming skills.
❑
You have some experience in C or C++, but not in a Microsoft Windows context and want to extend your skills to program for the Windows environment using the latest tools and technologies.
❑
You are a newcomer to programming and sufficiently keen to jump in and the deep end with C++. To be successful you need to have at least a rough idea of how your computer works, including the way in which the memory is organized and how data and instructions are stored.
Introduction
What This Book Covers My objective with this book is to teach you the essentials of C++ programming using both of the technologies supported by Visual C++ 2005. The book provides a detailed tutorial on both flavors of the C++ language, on native ISO/ANSI C++ Windows application development using the Microsoft Foundation Classes (MFC) and on the development of C++/CLI Windows applications using Windows Forms. Because of the importance and pervasiveness of database technology today, the book also includes introductions to the techniques you can use for accessing data sources in both MFC and Windows Forms applications. MFC applications are relatively coding-intensive compared to Windows Forms applications. This is because you create the latter using a highly developed design capability in Visual C++ 2005 that enables you to assemble the entire graphical user interface (GUI) for an application graphically and have all the code generated automatically. For this reason, there are more pages in the book devoted to MFC programming than to Windows Forms programming.
How This Book Is Structured The contents of the book are structured as follows:
xxxvi
❑
Chapter 1 introduces you to the basic concepts you need to understand for programming in C++ for native applications and for .NET Framework applications, together with the main ideas embodied in the Visual C++ 2005 development environment. It describes how you use the capabilities of Visual C++ 2005 for creating the various kinds of C++ applications you learn about in the rest of the book.
❑
Chapters 2 to 10 are dedicated to teaching you both versions of the C++ language, plus the basic ideas and techniques involved in debugging. The content of each of the Chapters 2 to 10 is structured in a similar way; the first half of each chapter deals with ISO/ANSI C++ topics, and the second half deals with C++/CLI.
❑
Chapter 11 discusses how Microsoft Windows applications are structured and describes and demonstrates the essential elements that are present in every Windows application. The chapter explains elementary examples of Windows applications using ISO/ANSI C++ and the Windows API and the MFC, as well as an example of a basic Windows Forms application in C++/CLI.
❑
Chapters 12 to 17 describe in detail the capabilities provided by the MFC for building a GUI. You learn how you create and use common controls to build the graphical user interface for your application and how you handle the events that result from user interactions with your program. In the process, you create a substantial working application. In addition to the techniques you learn for building a GUI, the application that you develop also shows you how you use MFC to print documents and to save them on disk.
❑
Chapter 18 teaches you the essentials you need to know for creating your own libraries using MFC. You learn about the different kinds of libraries you can create, and you develop working examples of these that work with the application that you have evolved over the preceding six chapters.
❑
In Chapters 19 and 20, you learn about accessing data sources in an MFC application. You gain experience in accessing a database in read-only mode; then you learn the fundamental programming techniques for updating a database using MFC. The examples use the Northwind database that can be downloaded from the Web, but you can also apply the techniques described to your own data source.
Introduction ❑
In Chapter 21 you work with Windows Forms and C++/CLI to build an example that teaches you how to create, customize, and use Windows Forms controls in an application. You gain practical experience by building an application incrementally throughout the chapter.
❑
Chapter 22 builds on the knowledge you gain in Chapter 21 and shows how the controls available for accessing data sources work, and how you customize them. You also learn how you can create an application for accessing a database with virtually no coding at all on your part.
All chapters include numerous working examples that demonstrate the programming techniques that are discussed. Every chapter concludes with a summary of the key points that were covered, and most chapters include a set of exercises at the end that you can attempt to apply what you have learned. Solutions to the exercises together with all the code from the book are available for download from the publisher’s Web site (see the “Source Code” section later in this Introduction for more details). The tutorial on the C++ language uses examples that are console programs with simple command-line input and output. This approach enables you to learn the various capabilities of C++ without getting bogged down in the complexities of Windows GUI programming. Programming for Windows is really only practicable after you have a thorough understanding of the programming language. If you want to keep things as simple as possible, you can just learn ISO/ANSI C++ programming in the first instance. Each of the chapters that cover the C++ language (Chapters 2 to 10) first discusses particular aspects of the capabilities of ISO/ANSI C++ followed by the new features introduced by C++/CLI in the same context. The reason for organizing things this way is that C++/CLI is defined as an extension to the ISO/ANSI standard language, so an understanding of C++/CLI is predicated on knowledge of ISO/ANSI C++. Thus, you can just read the ISO/ANSI topics in each of Chapters 2 to 20 and ignore the C++/CLI sections that follow. You then can to progress to Windows application development with ISO/ANSI C++ without having to keep the two versions of the language in mind. You can return to C++/CLI when you are comfortable with ISO/ANSI C++. Of course, you can also work straight through and add to your knowledge of both versions of the C++ language incrementally.
What You Need to Use This Book To use this book you need any of Visual Studio 2005 Standard Edition, Visual Studio 2005 Professional Edition, or Visual Studio 2005 Team System. Note that Visual C++ Express 2005 is not sufficient because the MFC is not included. Visual Studio 2005 requires Windows XP Service Pack 2 or Windows 2000 Service Pack 4.To install any of the three Visual Studio 2005 editions identified you need to have a 1 GHz processor with at least 256MB of memory and at least 1GB of space available on your system drive and 2GB available on the installation drive. To install the full MSDN documentation that comes with the product you’ll need an additional 1.8GB available on the installation drive. The database examples in the book use the Northwind Traders database. You can find the download for this database by searching for “Northwind Traders” on http://msdn.microsoft.com. Of course, you can also adapt the examples to work with a database of your choice. Most importantly, to get the most out of this book you need a willingness to learn, and a determination to master the most powerful programming tool for Windows applications presently available. You need the dedication to type in and work through all the examples and try out the exercises in the book. This sounds more difficult than it is, and I think you’ll be surprised how much you can achieve in a relatively
xxxvii
Introduction short period of time. Keep in mind that everybody who learns programming gets bogged down from time to time, but if you keep at it, things become clearer and you’ll get there eventually. This book helps you to start experimenting on your own and, from there, to become a successful C++ programmer.
Conventions To help you get the most from the text and keep track of what’s happening, a number of conventions are used throughout the book.
Try It Out The Try It Out is an exercise you should work through, following the text in the book.
How It Works After many Try It Outs, a How It Works section explains the code you’ve typed in detail.
Boxes like this one hold important, not-to-be forgotten information that is directly relevant to the surrounding text.
Tips, hints, tricks, and asides to the current discussion are offset and placed in italics like this. As for styles in the text: ❑
New terms and important words appear bold when first introduced.
❑
Keyboard strokes are shown like this: Ctrl+A.
❑
Filenames, URLs, and code within the text appear like so: persistence.properties.
❑
Code is presented in two different ways:
In code examples I highlight new and important code with a gray background. The gray highlighting is used for code that’s less important in the present context, or has been shown before.
Source Code As you work through the examples in this book, you may choose either to type in all the code manually or to use the source code files that accompany the book. All of the source code used in this book is available for download at http://www.wrox.com. At the site, simply locate the book’s title (either by using the Search box or by using one of the title lists) and click the Download Code link on the book’s detail page to obtain all the source code for the book.
xxxviii
Introduction Because many books have similar titles, you may find it easiest to search by ISBN; this book’s ISBN is 0-7645-7197-4 (changing to 978-0-7645-7197-8 as the new industry-wide 13-digit ISBN numbering system is phased in by January 2007). After you download the code, just decompress it with your favorite compression tool. Alternatively, you can go to the main Wrox code download page at http://www.wrox.com/dynamic/books/ download.aspx to see the code available for this book and all other Wrox books.
Errata We make every effort to ensure that there are no errors in the text or in the code. However, no one is perfect, and mistakes do occur. If you find an error in one of our books, like a spelling mistake or faulty piece of code, we would be very grateful for your feedback. By sending in errata you may save another reader hours of frustration and at the same time you will be helping us provide even higher quality information. To find the errata page for this book, go to http://www.wrox.com and locate the title using the Search box or one of the title lists. Then, on the book details page, click the Book Errata link. On this page you can view all errata that has been submitted for this book and posted by Wrox editors. A complete book list including links to each book’s errata is also available at www.wrox.com/misc-pages/booklist .shtml. If you don’t spot “your” error on the Book Errata page, go to www.wrox.com/contact/techsupport .shtml and complete the form there to send us the error you have found. We’ll check the information and, if appropriate, post a message to the book’s errata page and fix the problem in subsequent editions of the book.
p2p.wrox.com For author and peer discussion, join the P2P forums at p2p.wrox.com. The forums are a Web-based system for you to post messages relating to Wrox books and related technologies and interact with other readers and technology users. The forums offer a subscription feature to e-mail you topics of interest of your choosing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellow readers are present on these forums. At http://p2p.wrox.com you will find a number of different forums that will help you not only as you read this book, but also as you develop your own applications. To join the forums, just follow these steps:
1. 2. 3.
Go to p2p.wrox.com and click the Register link.
4.
You will receive an e-mail with information describing how to verify your account and complete the joining process.
Read the terms of use and click Agree. Complete the required information to join as well as any optional information you wish to provide and click Submit.
xxxix
1 Programming with Visual C++ 2005 Windows programming isn’t difficult. In fact, Microsoft Visual C++ 2005 makes it remarkably easy, as you’ll see throughout the course of this book. There’s just one obstacle in your path: Before you get to the specifics of Windows programming, you have to be thoroughly familiar with the capabilities of the C++ programming language, particularly the object-oriented aspects of the language. Object-oriented techniques are central to the effectiveness of all the tools that are provided by Visual C++ 2005 for Windows programming, so it’s essential that you gain a good understanding of them. That’s exactly what this book provides. This chapter gives you an overview of the essential concepts involved in programming applications in C++. You’ll take a rapid tour of the Integrated Development Environment (IDE) that comes with Visual C++ 2005. The IDE is straightforward and generally intuitive in its operation, so you’ll be able to pick up most of it as you go along. The best approach to getting familiar with it is to work through the process of creating, compiling, and executing a simple program. By the end of this chapter, you will have learned: ❑
What the principal components of Visual C++ 2005 are
❑
What the .NET Framework consists of and the advantages it offers
❑
What solutions and projects are and how you create them
❑
About console programs
❑
How to create and edit a program
❑
How to compile, link, and execute C++ console programs
❑
How to create and execute basic Windows programs
So power up your PC, start Windows, load the mighty Visual C++ 2005, and begin your journey.
Introduction You can read messages in the forums without joining P2P but in order to post your own messages, you must join. After you join, you can post new messages and respond to messages other users post. You can read messages at any time on the Web. If you would like to have new messages from a particular forum e-mailed to you, click the Subscribe to this Forum icon by the forum name in the forum listing. For more information about how to use the Wrox P2P, be sure to read the P2P FAQs for answers to questions about how the forum software works as well as many common questions specific to P2P and Wrox books. To read the FAQs, click the FAQ link on any P2P page.
xl
Chapter 1
The .NET Framework The .NET Framework is a central concept in Visual C++ 2005 as well as in all the other .NET development products from Microsoft. The .NET Framework consists of two elements: the Common Language Runtime (CLR) in which your application executes, and a set of libraries called the .NET Framework class libraries. The .NET Framework class libraries provide the functional support your code will need when executing with the CLR, regardless of the programming language used, so .NET programs written in C++, C#, or any of the other languages that support the .NET Framework all use the same .NET libraries. There are two fundamentally different kinds of C++ applications you can develop with Visual C++ 2005. You can write applications that natively execute on your computer. These applications will be referred to as native C++ programs. You write native C++ programs in the version of C++ that is defined by the ISO/ANSI language standard. You can also write applications to run under the control of the CLR in an extended version of C++ called C++/CLI. These programs will be referred to as CLR programs, or C++/CLI programs. The .NET Framework is not strictly part of Visual C++ 2005 but rather a component of the Windows operating system that makes it easier to build software applications and Web services. The .NET Framework offers substantial advantages in code reliability and security, as well as the ability to integrate your C++ code with code written in over 20 other programming languages that target the .NET Framework. A slight disadvantage of targeting the .NET Framework is that there is a small performance penalty, but you won’t notice this in the majority of circumstances.
The Common Language Runtime (CLR) The CLR is a standardized environment for the execution of programs written in a wide range of highlevel languages including Visual Basic, C#, and of course C++. The specification of the CLR is now embodied in the European Computer Manufacturers (ECMA) standard for the Common Language Infrastructure (CLI), ECMA-335, and also in the equivalent ISO standard, ISO/IEC 23271, so the CLR is an implementation of this standard. You can see why C++ for the CLR is referred to as C++/CLI — it’s C++ for the Common Language Infrastructure, so you are likely to see C++/CLI compilers on other operating systems that implement the CLI. Note that information on all ECMA standards is available from http://www.ecma-international. org and ECMA-335 is currently available as a free download.
The CLI is essentially a specification for a virtual machine environment that enables applications written in diverse high-level programming languages to be executed in different system environments without changing or recompiling the original source code. The CLI specifies a standard intermediate language for the virtual machine to which the high-level language source code is compiled. With the .NET Framework, this intermediate language is referred to as Microsoft Intermediate Language (MSIL). Code in the intermediate language is ultimately mapped to machine code by a just-in-time (JIT) compiler when you execute a program. Of course, code in the CLI intermediate language can be executed within any other environment that has a CLI implementation.
2
Programming with Visual C++ 2005 The CLI also defines a common set of data types called the Common Type System (CTS) that should be used for programs written in any programming language targeting a CLI implementation. The CTS specifies how data types are used within the CLR and includes a set of predefined types. You may also define your own data types, and these must be defined in a particular way to be consistent with the CLR, as you’ll see. Having a standardized type system for representing data allows components written in different programming languages to handle data in a uniform way and makes it possible to integrate components written in different languages to be integrated into a single application. Data security and program reliability is greatly enhanced by the CLR, in part because dynamic memory allocation and release for data is fully automatic but also because the MSIL code for a program is comprehensively checked and validated before the program executes. The CLR is just one implementation of the CLI specification that executes under Microsoft Windows on a PC; there will undoubtedly be other implementations of the CLI for other operating system environments and hardware platforms. You’ll sometimes find that the terms CLI and CLR used interchangeably, although it should be evident that they are not the same things. The CLI is a standard specification; the CLR is Microsoft’s implementation of the CLI.
Writing C++ Applications You have tremendous flexibility in the types of applications and program components that you can develop with Visual C++ 2005. As noted earlier in this chapter, you have two basic options for Windows applications: you can write code that executes with the CLR, and you can also write code that compiles directly to machine code and thus executes natively. For window-based applications targeting the CLR, you use Windows Forms as the base for the GUI provided by the .NET Framework libraries. Using Windows Forms enables rapid GUI development because you assemble the GUI graphically from standard components and have the code generated completely automatically. You then just need to customize the code that has been generated to provide the functionality you require. For natively executing code, you have several ways to go. One possibility is to use the Microsoft Foundation Classes (MFC) for programming the graphical user interface for your Windows application. The MFC encapsulates the Windows operating system Application Programming Interface (API) for GUI creation and control and greatly eases the process of program development. The Windows API originated long before the C++ language arrived on the scene so it has none of the object-oriented characteristics that would be expected if it were written today; however, you are not obliged to use the MFC. If you want the ultimate in performance, you can write your C++ code to access the Windows API directly. C++ code that executes with the CLR is described as managed C++ because data and code is managed by the CLR. In CLR programs, the release of memory that you have allocated dynamically for storing data is taken care of automatically, thus eliminating a common source of error in native C++ applications. C++ code that executes outside of the CLR is sometimes described by Microsoft as unmanaged C++ because the CLR is not involved in its execution. With unmanaged C++ you must take care of all aspects allocating and releasing memory during execution of your program yourself, and you also forego the enhanced security provided by the CLR. You’ll also see unmanaged C++ referred to as native C++ because it compiles directly to native machine code. Figure 1-1 shows the basic options you have for developing C++ applications.
3
Chapter 1
Managed C++
Native C++
Native C++
Framework Classes
MFC
Common Language Runtime
Operating System
Hardware
Figure 1-1
Figure 1-1 is not the whole story. An application can consist partly of managed C++ and partly of native C++, so you are not obliged to stick to one environment or the other. Of course, you do lose out somewhat by mixing the code, so you would choose to follow this approach only when necessary, such as when you want to convert an existing native C++ application to run with the CLR. You obviously won’t get the benefits inherent in managed C++ in the native C++ code, and there can also be appreciable overhead involved in communications between the managed and unmanaged code components. The ability to mix managed and unmanaged code can be invaluable, however, when you need to develop or extend existing unmanaged code but also want to obtain the advantages of using the CLR. Of course, for new applications you should decide whether you want to create it as a managed C++ application at the outset.
Learning Windows Programming There are always two basic aspects to interactive applications executing under Windows: you need code to create the Graphical User Interface (the GUI) with which the user interacts, and you need code to process these interactions to provide the functionality of the application. Visual C++ 2005 provides you with a great deal of assistance in both aspects of Windows application development. As you’ll see later in this chapter, you can create a working Windows program with a GUI without writing any code yourself at all. All the basic code to create the GUI can be generated automatically by Visual C++ 2005; however, it’s essential to understand how this automatically generated code works because you need to extend and modify it to make it do what you want, and to do that you need a comprehensive understanding of C++.
4
Programming with Visual C++ 2005 For this reason, you’ll first learn C++ — both the native C++ and C++/CLI versions of the language — without getting involved in Windows programming considerations. After you’re comfortable with C++, you’ll learn how you develop fully-fledged Windows applications using native C++ and C++/CLI. This means that while you are learning C++, you’ll be working with programs that just involved command line input and output. By sticking to this rather limited input and output capability, you’ll be able to concentrate of the specifics of how the C++ language works and avoid the inevitable complications involved in GUI building and control. After you become comfortable with C++, you’ll find that it’s an easy and natural progression to applying C++ to the development of Windows application programs.
Learning C++ Visual C++ 2005 fully supports two versions of C++ defined by two separate standards: ❑
The ISO/ANSI C++ standard is for implementing native applications — unmanaged C++. This version of C++ is supported on the majority of computer platforms.
❑
The C++/CLI standard is designed specifically for writing programs that target the CLR and is an extension to the ISO/ANSI C++.
Chapters 2 through 10 of this book teach you the C++ language. Because C++/CLI is an extension of ISO/ANSI C++, the first part of each chapter introduces elements of the ISO/ANSI C++ language; the second part explains the additional features that C++/CLI introduces. Writing programs in C++/CLI allows you to take full advantage of the capabilities of the .NET Framework, something that is not possible with programs written in ISO/ANSI C++. Although C++/CLI is an extension of ANSI/ISO C++, to be able to execute your program fully with the CLR means that it must conform to the requirements of the CLR. This implies that there are some features of ANSI/ISO C++ that you cannot use in your CLR programs. One example of this that you might deduce from what I have said up to now is that the dynamic memory allocation and release facilities offered by ISO/ANSI C++ are not compatible with the CLR; you must use the CLR mechanism for memory management and this implies that you must use C++/CLI classes, not native C++ classes.
The C++ Standards The ISO/ANSI standard is defined by the document ISO/IEC 14882 that is published by the American National Standards Institute (ANSI). ISO/ANSI standard C++ is the well-established version of C++ that has been around since 1998 and is supported by compilers on the majority of computer hardware platforms and operating systems. Programs that you write in ISO/ANSI C++ can be ported from one system environment to another reasonably easily, although the library functions that a program uses — particularly those related to building a graphical user interface — are a major determinant of how easy or difficult it will be. ISO/ANSI standard C++ has been the first choice of many professional program developers because it is so widely supported, and because is one of the most powerful programming languages available today. The ISO/ANSI standard for C++ can be purchased from http://www.iso.org. C++/CLI is a version of C++ that extends the ISO/ANSI standard for C++ to better support the Common Language Infrastructure (CLI) that is defined by the standard ECMA-355. The first draft of this standard appeared in 2003 and was developed from an initial technical specification that was produced by Microsoft to support the execution of C++ programs with the .NET Framework. Thus both the CLI and C++/CLI were originated by Microsoft in support of the .NET Framework. Of course, standardizing
5
Chapter 1 the CLI and C++/CLI greatly increases the likelihood of implementations in environments other than Windows. It’s important to appreciate that although C++/CLI is an extension of ISO/ANSI C++, there are features of ISO/ANSI C++ that you must not use when you want your program to execute fully under the control of the CLR. You’ll learn what these are as you progress through the book. The CLR offers substantial advantages over the native environment. By targeting your C++ programs at the CLR, your programs will be more secure and not prone to the potential errors you can make when using the full power of ISO/ANSI C++. The CLR also removes the incompatibilities introduced by various high-level languages by standardizing the target environment to which they are compiled and thus permit modules written in C++ to be combined with modules written in other languages such as C# or Visual Basic.
Console Applications As well as developing Windows applications, Visual C++ 2005 also allows you to write, compile, and test C++ programs that have none of the baggage required for Windows programs — that is, applications that are essentially character-based, command-line programs. These programs are called console applications in Visual C++ 2005 because you communicate with them through the keyboard and the screen in character mode. Writing console applications might seem as though you are being sidetracked from the main objective of Windows programming, but when it comes to learning C++ (which you do need to do before embarking on Windows-specific programming), it’s the best way to proceed. There’s a lot of code in even a simple Windows program, and it’s very important not to be distracted by the complexities of Windows when learning the ins and outs of C++. Therefore, in the early chapters of the book where you are concerned with how C++ works, you’ll spend time walking with a few lightweight console applications before you get to run with the heavyweight sacks of code in the world of Windows. While you’re learning C++, you’ll be able to concentrate on the language features without worrying about the environment in which you’re operating. With the console applications that you’ll write, you have only a text interface, but this will be quite sufficient for understanding all of C++ because there’s no graphical capability within the definition of the language. Naturally, I will provide extensive coverage of graphical user interface programming when you come to write programs specifically for Windows using Microsoft Foundation Classes (MFC) in native C++ applications and Windows Forms with the CLR. There are two distinct kinds of console applications and you’ll be using both. Win32 console applications compile to native code, and you’ll be using these to try out the capabilities of ISO/ANSI C++. CLR console applications target the CLR so you’ll be using these when you are working with the features of C++/CLI.
Windows Programming Concepts Our approach to Windows programming is to use all the tools that Visual C++ 2005 provides. The project creation facilities that are provided with Visual C++ 2005 can generate skeleton code for a wide variety of application programs automatically, including basic Windows programs. Creating a project is the starting point for all applications and components that you develop with Visual C++ 2005, and to get a
6
Programming with Visual C++ 2005 flavor of how this works, you’ll look at the mechanics of creating some examples, including an outline Windows program, later in this chapter. A Windows program has a different structure from that of the typical console program you execute from the command line, and it’s more complicated. In a console program, you can get input from the keyboard and write output back to the command line directly, whereas a Windows program can access the input and output facilities of the computer only by way of functions supplied by the Windows operating system; no direct access to the hardware resources is permitted. Because several programs can be active at one time under Windows, Windows has to determine which application a given raw input such as a mouse click or the pressing of a key on the keyboard is destined for and signal the program concerned accordingly. Thus the Windows operating system has primary control of all communications with the user. Also, the nature of the interface between a user and a Windows application is such that a wide range of different inputs is usually possible at any given time. A user may select any of a number of menu options, click a toolbar button, or click the mouse somewhere in the application window. A welldesigned Windows application has to be prepared to deal with any of the possible types of input at any time because there is no way of knowing in advance which type of input is going to occur. These user actions are received by the operating system in the first instance and are all regarded by Windows as events. An event that originates with the user interface for your application will typically result in a particular piece of your program code being executed. How program execution proceeds is therefore determined by the sequence of user actions. Programs that operate in this way are referred to as event-driven programs and are different from traditional procedural programs that have a single order of execution. Input to a procedural program is controlled by the program code and can occur only when the program permits it; therefore, a Windows program consists primarily of pieces of code that respond to events caused by the action of the user, or by Windows itself. This sort of program structure is illustrated in Figure 1-2. Each square block in Figure 1-2 represents a piece of code written specifically to deal with a particular event. The program may appear to be somewhat fragmented because of the number of disjointed blocks of code, but the primary factor welding the program into a whole is the Windows operating system itself. You can think of your program as customizing Windows to provide a particular set of capabilities. Of course, the modules servicing various external events, such as selecting a menu or clicking the mouse, all typically have access to a common set of application-specific data in a particular program. This application data contains information that relates to what the program is about — for example, blocks of text in an editor or player scoring records in a program aimed at tracking how your baseball team is doing — as well as information about some of the events that have occurred during execution of the program. This shared collection of data allows various parts of the program that look independent to communicate and operate in a coordinated and integrated fashion. I will go into this in much more detail later in the book. Even an elementary Windows program involves several lines of code, and with Windows programs that are generated by the application wizards that come with Visual C++ 2005, “several” turns out to be “many.” To simplify process of understanding how C++ works, you need a context that is as uncomplicated as possible. Fortunately, Visual C++ 2005 comes with an environment that is ready-made for the purpose.
7
Chapter 1
Events:
Keyboard Input
Press Left Mouse Button
Press Right Mouse Button
WINDOWS
Process Keyboard Input
Process Left Mouse Button
Process Right Mouse Button
Program Data Your Program
Figure 1-2
What Is the Integrated Development Environment? The Integrated Development Environment (IDE) that comes with Visual C++ 2005 is a completely selfcontained environment for creating, compiling, linking, and testing your C++ programs. It also happens to be a great environment in which to learn C++ (particularly when combined with a great book). Visual C++ 2005 incorporates a range of fully integrated tools designed to make the whole process of writing C++ programs easy. You will see something of these in this chapter, but rather than grind through a boring litany of features and options in the abstract, first take a look at the basics to get a view of how the IDE works and then pick up the rest in context as you go along.
8
Programming with Visual C++ 2005
Components of the System The fundamental parts of Visual C++ 2005, provided as part of the IDE, are the editor, the compiler, the linker, and the libraries. These are the basic tools that are essential to writing and executing a C++ program. Their functions are as follows.
The Editor The editor provides an interactive environment for you to create and edit C++ source code. As well as the usual facilities, such as cut and paste, which you are certainly already familiar with, the editor also provides color cues to differentiate between various language elements. The editor automatically recognizes fundamental words in the C++ language and assigns a color to them according to what they are. This not only helps to make your code more readable but also provides a clear indicator of when you make errors in keying such words.
The Compiler The compiler converts your source code into object code, and detects and reports errors in the compilation process. The compiler can detect a wide range of errors that are due to invalid or unrecognized program code, as well as structural errors, where, for example, part of a program can never be executed. The object code output from the compiler is stored in files called object files. There are two types of object code that the compiler produces. These object codes usually have names with the extension .obj.
The Linker The linker combines the various modules generated by the compiler from source code files, adds required code modules from program libraries supplied as part of C++, and welds everything into an executable whole. The linker can also detect and report errors — for example, if part of your program is missing or a non-existent library component is referenced.
The Libraries A library is simply a collection of pre-written routines that supports and extends the C++ language by providing standard professionally produced code units that you can incorporate into your programs to carry out common operations. The operations that are implemented by routines in the various libraries provided by Visual C++ 2005 greatly enhance productivity by saving you the effort of writing and testing the code for such operations yourself. I have already mentioned the .NET Framework library, and there are a number of others — too many to enumerate here — but I’ll mention the most important ones. The Standard C++ Library defines a basic set of routines common to all ISO/ANSI C++ compilers. It contains a wide range of routines including numerical functions such as calculating square roots and evaluating trigonometrical functions, character and string processing routines such as classifying characters and comparing character strings, and many others. You’ll get to know quite a number of these as you develop your knowledge of ISO/ANSI C++. There are also libraries that support the C++/CLI extensions to ISO/ANSI C++. Native window-based applications are supported by a library called the Microsoft Foundation Classes (MFC). The MFC greatly reduces the effort needed to build the graphical user interface for an application. You’ll see a lot more of the MFC when you finish exploring the nuances of the C++ language. Another library contains a set of facilities called Windows Forms that are roughly the equivalent of the MFC for window-based applications that executed with the .NET Framework. You’ll be seeing how you make use of Windows Forms to develop applications, too.
9
Chapter 1
Using the IDE All program development and execution in this book is performed from within the IDE. When you start Visual C++ 2005, notice an application window similar to that shown in Figure 1-3.
Figure 1-3
The window to the left in Figure 1-3 is the Solution Explorer window, the top-right window presently showing the Start page is the Editor window, and the window at the bottom is the Output window. The Solution Explorer window enables you to navigate through your program files and display their contents in the Editor window and to add new files to your program. The Solution Explorer window has up to three additional tabs (only two are shown in Figure 1-3) that display Class View, Resource View, and Property Manager for your application, and you can select which tabs are to be displayed from the View menu. The Editor window is where you enter and modify source code and other components of your application. The Output window displays messages that result from compiling and linking your program.
Toolbar Options You can choose which toolbars are displayed in your Visual C++ window by right-clicking in the toolbar area. A pop-up menu with a list of toolbars (Figure 1-4) appears, and the toolbars that are currently displayed have check marks alongside.
10
Programming with Visual C++ 2005
Figure 1-4
This is where you decide which toolbars are visible at any one time. You can make your set of toolbars the same as those shown in Figure 1-3 by making sure the Build, Class Designer, Debug, Standard, and View Designer menu items are checked. Clicking in the gray area to the left of a toolbar checks it if it unchecked and results in it being displayed; clicking a check mark hides the corresponding toolbar. You don’t need to clutter up the application window with all the toolbars you think you might need at some time. Some toolbars appear automatically when required, so you’ll probably find that the default toolbar selections are perfectly adequate most of the time. As you develop your applications, from time to time you might think it would be more convenient to have access to toolbars that aren’t displayed. You can change the set of toolbars that are visible whenever it suits you by right-clicking in the toolbar area and choosing from the context menu.
11
Chapter 1 Similar to many other Windows applications, the toolbars that make up Visual C++ 2005 come complete with tooltips. Just let the mouse pointer linger over a toolbar button for a second or two and a white label displays the function of that button.
Dockable Toolbars A dockable toolbar is one that you can drag around with the mouse to position at a convenient place in the window. When it is placed in any of the four borders of the application, it is said to be docked and looks similar to the toolbars you see at the top of the application window. The toolbar on the upper line of toolbar buttons that contains the disk icons and the text box to the right of a pair of binoculars is the Standard toolbar. You can drag this away from the toolbar by placing the cursor on it and dragging it with the mouse while you hold down the left mouse button. It then appears as a separate window you can position anywhere. If you drag any dockable toolbar away from its docked position, it looks like the Standard toolbar you see in Figure 1-5, enclosed in a little window — with a different caption. In this state, it is called a floating toolbar. All the toolbars that you see in Figure 1-3 are dockable and can be floating, so you can experiment with dragging any of them around. You can position them in docked positions where they revert to their normal toolbar appearance. You can dock a dockable toolbar at any side of the main window.
Figure 1-5
You’ll become familiar with many of the toolbar icons that Visual C++ 2005 uses from other Windows applications, but you may not appreciate exactly what these icons do in the context of Visual C++, so I’ll describe them as we use them. Because you’ll use a new project for every program you develop, looking at what exactly a project is and understanding how the mechanism for defining a project works is a good place to start finding out about Visual C++ 2005.
Documentation There will be plenty of occasions when you’ll want to find out more information about Visual C++ 2005. The Microsoft Development Network (MSDN) Library provides comprehensive reference material on all the capabilities on Visual C++ 2005 and more besides. When you install Visual C++ 2005 onto your machine, there is an option to install part or all of the MSDN documentation. If you have the disk space available I strongly recommend that you install the MSDN Library.
12
Programming with Visual C++ 2005 Press the F1 function to browse the MSDN Library. The Help menu also provides various routes into the documentation. As well as offering reference documentation, the MSDN Library is a useful tool when dealing with errors in your code, as you’ll see later in this chapter.
Projects and Solutions A project is a container for all the things that make up a program of some kind — it might be a console program, a window-based program, or some other kind of program, and it usually consists of one or more source files containing your code plus possibly other files containing auxiliary data. All the files for a project are stored in the project folder and detailed information about the project is stored in an XML file with the extension .vcproj that is also in the project folder. The project folder also contains other folders that are used to store the output from compiling and linking your project. The idea of a solution is expressed by its name, in that it is a mechanism for bringing together all the programs and other resources that represent a solution to a particular data processing problem. For example, a distributed order entry system for a business operation might be composed of several different programs that could each be developed as a project within a single solution; therefore, a solution is a folder in which all the information relating to one or more projects is stored, so one or more project folders are subfolders of the solution folder. Information about the projects in a solution is stored in two files with the extensions .sln and .suo. When you create a project, a new solution is created automatically unless you elect to add the project to an existing solution. When you create a project along with a solution, you can add further projects to the same solution. You can add any kind of project to an existing solution, but you would usually add only a project that was related in some way to the existing project or projects in the solution. Generally, unless you have a good reason to do otherwise, each of your projects should have its own solution. Each example you create with this book will be a single project within its own solution.
Defining a Project The first step in writing a Visual C++ 2005 program is to create a project for it using the File > New > Project menu option from the main menu or you can press Ctrl+Shift+N. As well as containing files that define all the code and any other data that goes to make up your program, the project XML file in the project folder also records the Visual C++ 2005 options you’re using. Although you don’t need to concern yourself with the project file — it is entirely maintained by the IDE — you can browse it if you want to see what the contents are, but take care not to modify it accidentally. That’s enough introductory stuff for the moment. It’s time to get your hands dirty.
Try It Out
Creating a Project for a Win32 Console Application
You’ll now take a look at creating a project for a console application. First select File > New > Project to bring up the New Project dialog box, shown in Figure 1-6.
13
Chapter 1
Figure 1-6
The left pane in the New Project dialog box displays the types of projects you can create; in this case, click Win32. This also identifies an application wizard that creates the initial contents for the project. The right pane displays a list of templates available for the project type you have selected in the left pane. The template you select is used by the application wizard when creating the files that make up the project. In the next dialog box, you have an opportunity to customize the files that are created when you click the OK button in this dialog box. For most of the type/template options, a basic set of program source modules are created automatically. You can now enter a suitable name for your project by typing into the Name: edit box — for example, you could call this one Ex1_01, or you can choose your own project name. Visual C++ 2005 supports long file names, so you have a lot of flexibility. The name of the solution folder appears in the bottom edit box and, by default, the solution folder has the came name as the project. You can change this if you want. The dialog box also allows you to modify the location for the solution that contains your project — this appears in the Location: edit box. If you simply enter a name for your project, the solution folder is automatically set to a folder with that name, with the path shown in the Location: edit box. By default the solution folder is created for you if it doesn’t already exist. If you want to specify a different path for the solution folder, just enter it in the Location: edit box. Alternatively, you can use the Browse button to select another path for your solution. Clicking the OK button displays the Win32 Application Wizard dialog box shown in Figure 1-7. This dialog box explains the settings currently in effect. If you click the Finish button, the wizard creates all the project files based on this. In this case you can click Applications Settings on the left to display the Application Settings page of the wizard shown in Figure 1-8.
14
Programming with Visual C++ 2005
Figure 1-7
Figure 1-8
The Application Settings page allows you to choose options that you want to apply to the project. For most of the projects you’ll be creating when you are learning the C++ language, you select the Empty project checkbox, but here you can leave things as they are and click the Finish button. The application wizard then creates the project with all the default files.
15
Chapter 1 The project folder will have the name that you supplied as the project name and will hold all the files making up the project definition. If you didn’t change it, the solution folder has the same name as the project folder and contains the project folder plus the files defining the contents of the solution. If you use Windows Explorer to inspect the contents of the solution folder, you’ll see that it contains three files: ❑
A file with the extension .sln that records information about the projects in the solution.
❑
A file with the extension .suo in which user options that apply to the solution will be recorded.
❑
A file with the extension .ncb that records data about Intellisense for the solution. Intellisense is the facility that provides auto-completion and prompting for code in the Editor window as you enter it.
If you use Windows Explorer to look in the project folder, notice there are six files initially, including a file with the name ReadMe.txt that contains a summary of the contents of the files that have been created for the project. The one file that ReadMe.txt may not mention is a file with a compound name of the form Ex1_01.vcproj.ComputerName.UserName.user used to store options you set for the project. The project you have created will automatically open in Visual C++ 2005 with the left pane as in Figure 1-9. I have increased the width of this pane so that you can see the complete names on the tabs.
Figure 1-9
The Solution Explorer tab presents a view of all the projects in the current solution and the files they contains — here there is just one project of course. You can display the contents of any file as an additional tab in the Editor pane just by double-clicking in name in the Solution Explorer tab. In the edit pane you can switch instantly between any of the files that have been displayed just by clicking on the appropriate tab.
16
Programming with Visual C++ 2005 The Class View tab displays the classes defined in your project and also shows the contents of each class. You don’t have any classes in this application, so the view is empty. When we discuss classes, you will see that you can use the Class View tab to move around the code relating to the definition and implementation of all your application classes quickly and easily. The Property Manager tab shows the properties that have been set for the Debug and Release versions of your project. I’ll explain these versions a little later in this chapter. You can change any of the properties shown by right-clicking a property and selecting Properties from the context menu; this displays a dialog box where you can set the project property. You can also press Alt+F7 to display the properties dialog box at any time; I’ll also discuss this in more detail when we go into the Debug and Release versions of a program. The Resource View shows the dialog boxes, icons, menus toolbars, and other resources that are used by the program. Because this is a console program, no resources are used; however, when you start writing Windows applications, you’ll see a lot of things here. Through this tab you can edit or add to the resources available to the project. Like most elements of the Visual C++ 2005 IDE, the Solution Explorer and other tabs provide contextsensitive pop-up menus when you right-click items displayed in the tab and in some cases in the empty space in the tab, too. If you find that the Solution Explorer pane gets in your way when writing code, you can hide it by clicking the Autohide icon. To redisplay it, click the name tab on the left of the IDE window.
Modifying the Source Code The Application wizard generates a complete Win32 console program that you can compile and execute. Unfortunately, the program doesn’t do anything as it stands, so to make it a little more interesting you need to change it. If it is not already visible in the Editor pane, double-click Ex1_01.cpp in the Solution Explorer pane. This file is the main source file for the program that the Application wizard generated and it looks like that shown in Figure 1-10.
Figure 1-10
17
Chapter 1 If the line numbers are not displayed on your system, select Tools > Options from the main menu to display the Options dialog box. If you extend the C/C++ option in the right pane and select General from the extended tree, you can select Line Numbers in the right pane of the dialog box. I’ll first give you a rough guide to what this code in Figure 1-10 does, and you’ll see more on all of these later. The first two lines are just comments. Anything following “//”in a line is ignored by the compiler. When you want to add descriptive comments in a line, precede your text by “//”. Line 4 is an #include directive that adds the contents of the file stdafx.h to this file in place of this #include directive. This is the standard way of adding the contents of .h source files to a .cpp source file a in a C++ program. Line 7 is the first line of the executable code in this file and the beginning of the function _tmain(). A function is simply a named unit of executable code in a C++ program; every C++ program consists of at least one — and usually many more — functions. Lines 8 and 10 contain left and right braces, respectively, that enclose all the executable code in the function _tmain(). The executable code is, therefore, just the single line 10 and all this does is end the program. Now you can add the following two lines of code in the Editor window: // Ex1_01.cpp : Defines the entry point for the console application. // #include “stdafx.h” #include
int _tmain(int argc, _TCHAR* argv[]) { std::cout << “Hello world!\n”; return 0; }
The unshaded lines are the ones generated for you. The new lines you should add are shown shaded. To introduce each new line, place the cursor at the end on the text on the preceding line and press Enter to create an empty line in which you can type the new code. Make sure it is exactly as shown in the preceding example; otherwise, the program may not compile. The first new line is an #include directive that adds the contents of one of the standard libraries for ISO/ANSI C++ to the source file. The library defines facilities for basic I/O operations, and the one you are using in the second line that you added writes output to the command line. std::cout is the name of the standard output stream and you write the string “Hello world!\n” to std::cout in the second addition statement. Whatever appears between the pair of double quote characters is written to the command line.
Building the Solution To build the solution, press F7 or select the Build > Build Solution menu item. Alternatively, you can click the toolbar button corresponding to this menu item. The toolbar buttons for the Build menu may not display, but you can easily fix this by right-clicking in the toolbar area and selecting the Build
18
Programming with Visual C++ 2005 toolbar from those in the list. The program should then compile successfully. If there are errors, ensure you didn’t make an error while entering the new code, so check the two new lines very carefully.
Files Created by Building a Console Application After the example has been built without error, take a look in the project folder by using Windows Explorer to see a new subfolder called Debug. This folder contains the output of the build you just performed on the project. Notice that this folder contains several files. Other than the .exe file, which is your program in executable form, you don’t need to know much about what’s in these files. In case you’re curious, however, let’s do a quick run-through of what the more interesting ones are for. File Extension
Description
.exe
This is the executable file for the program. You get this file only if both the compile and link steps are successful.
.obj
The compiler produces these object files containing machine code from your program source files. These are used by the linker, along with files from the libraries, to produce your .exe file.
.ilk
This file is used by the linker when you rebuild your project. It enables the linker to incrementally link the object files produced from the modified source code into the existing .exe file. This avoids the need to re-link everything each time you change your program.
.pch
This is a pre-compiled header file. With pre-compiled headers, large tracts of code that are not subject to modification (particularly code supplied by C++ libraries) can be processed once and stored in the .pch file. Using the .pch file substantially reduces the time needed to rebuild your program.
.pdb
This file contains debugging information that is used when you execute the program in debug mode. In this mode, you can dynamically inspect information that is generated during program execution.
.idb
Contains information used when you rebuild the solution.
Debug and Release Versions of Your Program You can set a range of options for a project through the Project > Ex1_01 Properties menu item. These options determine how your source code is processed during the compile and link stages. The set of options that produces a particular executable version of your program is called a configuration. When you create a new project workspace, Visual C++ 2005 automatically creates configurations for producing two versions of your application. One version, called the Debug version, includes information that helps you debug the program. With the Debug version of your program you can step through the code when things go wrong, checking on the data values in the program. The other, called the Release version, has no debug information included and has the code optimization options for the compiler turned on to provide you with the most efficient executable module. These two configurations are sufficient for your needs throughout this book, but when you need to add other configurations for an application, you can do so through the Build > Configuration Manager menu. Note that this menu item won’t appear if you haven’t got a project loaded. This is obviously not a problem, but might be confusing if you’re just browsing through the menus to see what’s there.
19
Chapter 1 You can choose which configuration of your program to work with by selecting the configuration from the Active solution configuration drop-down list in the Configuration Manager dialog box, as shown in Figure 1-11.
Figure 1-11
Select the configuration you want to work with from the list and then click the Close button. While you’re developing an application, you’ll work with the debug configuration. After your application has been tested using the debug configuration and appears to be working correctly, you typically rebuild the program as a release version; this produces optimized code without the debug and trace capability, so the program runs faster and occupies less memory.
Executing the Program After you have successfully compiled the solution, you can execute your program by pressing Ctrl+F5. You should see the window shown in Figure 1-12.
Figure 1-12
20
Programming with Visual C++ 2005 As you see, you get the text that was between the double quotes written to the command line. The “\n” that appeared at the end of the text string is a special sequence called an escape sequence that denotes a newline character. Escape sequences are used to represent characters in a text string that you cannot enter directly from the keyboard.
Try It Out
Creating an Empty Console Project
The previous project contained a certain amount of excess baggage that you don’t need when working with simple C++ language examples. The precompiled headers option chosen by default resulted in the stdafx.h file being created in the project. This is a mechanism for making the compilation process more efficient when there are a lot of files in a program but this won’t be necessary for many of our examples. In these instances you start with an empty project to which you can add your own source files. You can see how this works by creating a new project in a new solution for a Win32 console program with the name Ex1_02. After you have entered the project name and clicked the OK button, click on Applications Settings on the right side of the dialog box that follows. You can then select Empty project from the additional options, as Figure 1-13 shows.
Figure 1-13
When you click the Finish button, the project is created as before, but this time without any source files. Next you add a new source file to the project. Right-click the Solution Explorer pane and then select Add > New Item from the context menu. A dialog box displays; click Code in the right pane, and C++ File(.cpp) in the left pane. Enter the file name as Ex1_02, as shown in Figure 1-14.
21
Chapter 1
Figure 1-14
When you click the Add button, the new file is added to the project and is displayed in the Editor window. Of course, the file is empty so nothing will be displayed; enter the following code in the Editor window: // Ex1_02.cpp A simple console program #include // Basic input and output library int main() { std::cout << “This is a simple program that outputs some text.” << std::endl; std::cout << “You can output more lines of text” << std::endl; std::cout << “just by repeating the output statement like this.” << std::endl; return 0; // Return to the operating system }
Note the automatic indenting that occurs as you type the code. C++ uses indenting to make programs more readable, and the editor automatically indents each line of code that you enter, based on what was in the previous line. You can also see the syntax color highlighting in action as you type. Some elements of the program are shown in different colors as the editor automatically assigns colors to language elements depending on what they are. The preceding code is the complete program. You probably noticed a couple of differences compared to the code generated by the Application wizard in the previous example. There’s no #include directive for the stdafx.h file. You don’t have this file as part of the project here because you are not using the precompiled headers facility. The name of the function here is main; before it was _tmain. In fact all ISO/ANSI C++ programs start execution in a function called main(). Microsoft also provides for this function to be called wmain when Unicode characters are used and the name _tmain is defined to be
22
Programming with Visual C++ 2005 either main or wmain, depending on whether or not the program is going to use Unicode characters. For the previous example, the name _tmain is defined behind the scenes to be main. You use the name main in all the ISO/ANSI C++ examples. The output statements are a little different. The first statement in main() is: std::cout << “This is a simple program that outputs some text.” << std::endl;
You have two occurrences of the << operator, and each one sends whatever follows to std::cout, which is the standard output stream. First the string between double quotes is sent to the stream and then std::endl where std::endl is defined in the standard library as a newline character. Earlier you used the escape sequence \n for a newline character within a string between double quotes. You could have written the preceding statement as: std::cout << “This is a simple program that outputs some text.\n”;
I should explain why the line is shaded, where the previous line of code is not. Where I repeat a line of code for explanation purposes I show it unshaded. The preceding line of code is new and does not appear earlier so I have shown it shaded. You can now build this project in the same way as the previous example. Note that any open source files in the Editor pane are saved automatically if you have not already saved them. When you have compiled the program successfully, press Ctrl+F5 to execute it. The window shown in Figure 1-15 displays.
Figure 1-15
Dealing with Errors Of course, if you didn’t type the program correctly, you get errors reported. To show how this works, you could deliberately introduce an error into the program. If you already have errors of your own, you can use those to perform this exercise. Go back to the Editor pane and delete the semicolon at the end of the second-to-last line between the braces (line 8); then rebuild the source file. The Output pane at the bottom of the application window includes the error message: C2143: syntax error : missing ‘;’ before ‘return’
Every error message during compilation has an error number that you can look up in the documentation. Here, the problem is obvious, ; however, in more obscure cases, the documentation may help you figure out what is causing the error. To get the documentation on an error, click the line in the Output pane that contains the error number and then press F1. A new window displays containing further information about the error. You can try it with this simple error, if you like.
23
Chapter 1 When you have corrected the error, you can then rebuild the project. The build operation works efficiently because the project definition keeps track of the status of the files making up the project. During a normal build, Visual C++ 2005 recompiles only the files that have changed since the program was last compiled or built. This means that if your project has several source files and you’ve edited only one of the files since the project was last built, only that file is recompiled before linking to create a new .exe file. You’ll also use CLR console programs, so the next section shows you what a CLR console project looks.
Try It Out
Creating a CLR Console Project
Press Ctrl+Shift+N to display the New Project dialog box; then select the project type as CLR and the template as CLR Console Application, as shown in Figure 1-16.
Figure 1-16
Enter the name as Ex1_03. When you click the OK button, the files for the project are created. There are no options for a CLR console project, so you always start with the same set of files in a project with this template. If you want an empty project — something you won’t need with this book — there’s a separate template for this. If you look at the Solution Explorer pane shown in Figure 1-17, you see there are some extra files compare to a Win32 console project. There are a couple of files in the virtual Resource Files folder. The .ico file stores an icon for the application that is displayed when the program is minimized; the .rc file records the resources for the application — just the icon in this case.
24
Programming with Visual C++ 2005
Figure 1-17
There is also a file with the name AssemblyInfo.cpp. Every CLR program consists of one or more assemblies where an assembly is a collection of code and resources that form a functional unit. An assembly also contains extensive data for the CLR; there are specifications of the data types that are being used, versioning information about the code, and information that determines if the contents of the assembly can be accessed from another assembly. In short, an assembly is a fundamental building block in all CLR programs. If the source code in the Ex1_03.cpp file is not displayed in the Editor window, double-click the file name in the Solution Explorer pane. It should look like Figure 1-18.
Figure 1-18
25
Chapter 1 It has the same #include directive as the default native C++ console program because CLR programs use precompiled headers for efficiency. The next line is new: using namespace System;
The .NET library facilities are all defined within a namespace, and all the standard sort of stuff you are likely to use is in a namespace with the name System. This statement indicates the program code that follows uses the System namespace, but what exactly is a namespace? A namespace is a very simple concept. Within your program code and within the code that forms the .NET libraries, names have to be given to lots of things — data types, variables, and blocks of code called functions all have to have names. The problem is that if you happen to invent a name that is already used in the library, there’s potential for confusion. A namespace provides a way of getting around this problem. All the names in the library code that is defined within the System namespace are implicitly prefixed with the namespace name. So, a name such as String in the library is really System::String. This means that if you have inadvertently used the name String for something in your code, you can use System::String to refer String from the .NET library. The two colons[md]:: — are an operator called the scope resolution operator. Here the scope resolution operator separates the namespace name System from the type name String. You have seen this in the native C++ examples earlier in this chapter with std::cout and std::endl. This is the same story — std is the namespace name for native C++ libraries, and cout and endl are the names that have been defined within the std namespace to represent the standard output stream and the newline character, respectively. In fact, the using namespace statement in the example allows you to use any name from the System namespace without having to use the namespace name as a prefix. If you did end up with a name conflict between a name you have defined and a name in the library, you could resolve the problem by removing the using namespace statement and explicitly qualify the name from the library with the namespace name. You’ll learn more about namespaces in Chapter 2. You can compile and execute the program by pressing Ctrl+F5. The output is as shown in Figure 1-19.
Figure 1-19
The output is the same as from the first example. This output is produced by the line: Console::WriteLine(L”Hello World”);
26
Programming with Visual C++ 2005 This uses a .NET library function to write the information between the double quotes to the command line, so this is the CLR equivalent of the native C++ statement that you added to Ex1_01: std::cout << “Hello world!\n”;
It is more immediately apparent what the CLR statement does than the native C++ statement.
Setting Options in Visual C++ 2005 There are two sets of options you can set. You can set options that apply to the tools provided by Visual C++ 2005, which apply in every project context. Also, you can set options that are specific to a project and determine how the project code is to be processed when it is compiled and linked. Options are set through the Options dialog box that’s displayed when you select Tools > Options from the main menu. The Options dialog box is shown in Figure 1-20.
Figure 1-20
Clicking the plus sign (+) for any of the items in the left pane displays a list of subtopics. Figure 1-20 shows the options for the General subtopic under Projects and Solutions. The right pane displays the options you can set for the topic you have select in the left pane. You should concern yourself with only a few of these at this time, but you’ll find it useful to spend a little time browsing the range of options available to you. Clicking the Help button (with the ?) at the top right of the dialog box displays an explanation of the current options. You probably want to choose a path to use as a default when you create a new project, and you can do this through the first option shown in Figure 1-20. Just set the path to the location where you want your projects and solutions stored. You can set options that apply to every C++ project by selecting the Projects and Solutions > VC++ Project Settings topic in the left pane. You can also set options specific to the current project through the Project > Properties menu item in the main menu. This menu item label is tailored to reflect the name of the current project.
27
Chapter 1
Creating and Executing Windows Applications Just to show how easy it’s going to be, now create two working Windows applications. You’ll create a native C++ application using MFC and then you’ll create a Windows Forms application that runs with the CLR. I’ll defer discussion of the programs that you generate until I’ve covered the necessary ground for you to understand it in detail. You will see, though, that the processes are straightforward.
Creating an MFC Application To start with, if an existing project is active — as indicated by the project name appearing in the title bar of the Visual C++ 2005 main window — you can select Close Solution from the File menu. Alternatively, you can create a new project and have the current solution closed automatically. To create the Windows program select New > Project from the File menu or press Ctrl+Shift+N; then choose the project type as MFC and select MFC Application as the project template. You can then enter the project name as Ex1_04 as shown in Figure 1-21.
Figure 1-21
When you click the OK button, the MFC Application Wizard dialog box is displayed. The dialog box has a range of options that let you choose which features you’d like to have included in your application. These are identified by the items in the list on the right of the dialog box, as Figure 1-22 shows. You’ll get to use many of these in examples later on. You can ignore all these options in this instance and just accept the default settings, so click the Finish button to create the project with the default settings. The Solution Explorer pane in the IDE window looks like Figure 1-23.
28
Programming with Visual C++ 2005
Figure 1-22
Figure 1-23
29
Chapter 1 Note that I have hidden the Property Manager tab by right-clicking it and selecting Hide, so it doesn’t appear in Figure 1-23. The list shows a large number of files that have been created. You need plenty of space on your hard drive when writing Windows programs! The files with the extension .cpp contain executable C++ source code, and the .h files contain C++ code consisting of definitions that are used by the executable code. The .ico files contain icons. The files are grouped into the subfolders you can see for ease of access. These aren’t real folders, though, and they won’t appear in the project folder on your disk. If you now take a look at the Ex1_04 solution folder using Windows Explorer or whatever else you may have handy for looking at the files on your hard disk, notice that you have generated a total of 24 files. Three of these are in the solution folder, a further 17 are in the project folder and four more are in a subfolder, res, to the project folder. The files in the res subfolder contain the resources used by the program — such as the menus and icons used in the program. You get all this as a result of just entering the name you want to assign to the project. You can see why, with so many files and file names being created automatically, a separate directory for each project becomes more than just a good idea. One of the files in the Ex1_04 project directory is ReadMe.txt, and this provides an explanation of the purpose of each of the files that the MFC Application wizard has generated. You can take a look at it if you want, using Notepad, WordPad, or even the Visual C++ 2005 editor. To view it in the Editor window, double-click it in the Solution Explorer pane.
Building and Executing the MFC Application Before you can execute the program, you have to build the project — meaning, compile the source code and link the program modules. You do this in exactly the same way that you did with the console application example. To save time, press Ctrl+F5 to get the project built and then executed in a single operation. After the project has been built, the Output window indicates that there are no errors and the executable starts running. The window for the program you’ve generated is shown in Figure 1-24.
Figure 1-24
30
Programming with Visual C++ 2005 As you see, the window is complete with menus and a toolbar. Although there is no specific functionality in the program — that’s what you need to add to make it your program — all the menus work. You can try them out. You can even create further windows by selecting New from the File menu. I think you’ll agree that creating a Windows program with the MFC Application wizard hasn’t stressed too many brain cells. You’ll need to get a few more ticking away when you come to developing the basic program you have here into a program that does something more interesting, but it won’t be that hard. Certainly, for many people, writing a serious Windows program the old-fashioned way, without the aid of Visual C++ 2005, required at least a couple of months on a fish diet before making the attempt. That’s why so many programmers used to eat sushi. That’s all gone now with Visual C++ 2005. You never know, however, what’s around the corner in programming technology. If you like sushi, it’s best to continue with it to be on the safe side.
Creating a Windows Forms Application This is a job for another application wizard. So create yet another new project, but this time select the type as CLR in the left pane of the New Project dialog box and the template as Windows Forms Application. You can then enter the project name as Ex1_05 as shown in Figure 1-25.
Figure 1-25
There are no options to choose from in this case, so click the OK button to create the project. The Solution Explorer pane in Figure 1-26 shows the files that have been generated for this project.
31
Chapter 1
Figure 1-26
There are considerably fewer files in this project — if you look in the directories, you’ll see there are a total of 15 including the solution files. One reason for this is the initial GUI is much simpler than the native C++ application using MFC. The Windows Forms application has no menus or toolbars, and there is only one window. Of course, you can add all these things quite easily, but the wizard for a Windows Forms application does not assume you want them from the start. The Editor window looks rather different as Figure 1-27 shows.
32
Programming with Visual C++ 2005
Figure 1-27
The Editor window shows an image of the application window rather than code. The reason for this is that developing the GUI for a Windows Forms is oriented towards a graphical design approach rather than a coding approach. You add GUI components to the application window by dragging or placing them there graphically, and Visual C++ 2005 automatically generates the code to display them. If you press Ctrl+Alt+X or select View > Toolbox, you’ll see an additional window displayed showing a list of GUI components as in Figure 1-28.
33
Chapter 1
Figure 1-28
The Toolbox window presents a list of standard components that you can add to a Windows Forms application. You can try adding some buttons to the window for Ex1_05. Click Button in the Toolbox window list and then click in the client area of the Ex1_05 application window that is displayed in the Editor window where you want the button to be placed. You can adjust the size of the button by dragging its borders, and you can reposition the button by dragging it around. You can also change the caption just by typing — try entering Start on the keyboard and then press Enter. The caption changes and along the way another window displays, showing the properties for the button. I won’t go into these now, but essentially these are the specifications that affect the appearance of the button, and you can change these to suit your application. Try adding another button with the caption Stop, for example. The Editor window will look like Figure 1-29.
34
Programming with Visual C++ 2005
Figure 1-29
You can graphically edit any of the GUI components at any time, and the code adjusts automatically. Try adding a few other components in the same way and then compile and execute the example by pressing Ctrl+F5. The application window displays in all its glory. Couldn’t be easier, could it?
Summar y In this chapter, you have run through the basic mechanics of using Visual C++ 2005 to create applications of various kinds. You created and executed native and CLR console programs, and with the help of the application wizards, you created an MFC-based Windows program and a Windows Forms program that executes with the CLR. The points from this chapter that you should keep in mind are: ❑
The Common Language Runtime (CLR) is the Microsoft implementation of the Common Language Infrastructure (CLI) standard.
❑
The .NET Framework comprises the CLR plus the .NET libraries that support applications targeting the CLR.
❑
Native C++ applications are written the ISO/ANSI C++ language.
❑
Programs written in the C++/CLI language execute with the CLR.
35
Chapter 1 ❑
A solution is a container for one or more projects that form a solution to an information-processing problem of some kind.
❑
A project is a container for the code and resource elements that make up a functional unit in a program.
❑
An assembly is a fundamental unit in a CLR program. All CLR programs are made up of one or more assemblies.
Starting with the next chapter, you’ll use console applications extensively throughout the first half of the book. All the examples illustrating how C++ language elements are used are executed using either Win32 or CLR console applications. You will return to the Application Wizard for MFC-based programs as soon as you have finished delving into the secrets of C++.
36
2 Data, Variables, and Calculations In this chapter, you’ll get down to the essentials of programming in C++. By the end of the chapter, you’ll be able to write a simple C++ program of the traditional form: input-process-output. As I said in the previous chapter, I’ll first discuss the ANSI/ISO C++ language features and then cover any additional or different aspects of the C++/CLI language. As you explore aspects of the language using working examples, you’ll have an opportunity to get some additional practice with the Visual C++ Development Environment. You should create a project for each of the examples before you build and execute them. Remember that when you are defining projects in this chapter and the following chapters through to Chapter 10, they are all console applications. In this chapter you will learn about: ❑
C++ program structure
❑
Namespaces
❑
Variables in C++
❑
Defining variables and constants
❑
Basic input from the keyboard and output to the screen
❑
Performing arithmetic calculations
❑
Casting operands
❑
Variable scope
Chapter 2
The Structure of a C++ Program Programs that run as console applications under Visual C++ 2005 read data from the command line and output the results to the command line. To avoid having to dig into the complexities of creating and managing application windows before you have enough knowledge to understand how they work, all the examples that you’ll write to understand how the C++ language works will be console programs, either Win32 console programs or .NET console programs. This enables you to focus entirely on the C++ language in the first instance; after you have mastered that, you’ll be ready to deal with creating and managing application windows. You’ll first look at how console programs are structured. A program in C++ consists of one or more functions. In Chapter 1, you saw an example that was a Win32 console program consisting simply of the function main(), where main is the name of the function. Every ANSI/ISO standard C++ program contains the function main(), and all C++ programs of any size consist of several functions — the main() function where execution of the program starts, plus a number of other functions. A function is simply a self-contained block of code with a unique name that you invoke for execution by using the name of the function. As you saw in Chapter 1, a Win32 console program that is generated by the Application wizard has a main function with the name _tmain. This is a programming device to allow the name to be main or wmain, depending on whether or not the program is using Unicode characters. The names wmain and _tmain are Microsoft-specific. The name for the main function conforming to the ISO/ANSI standard for C++ is main. I’ll use the name main for all our ISO/ANSI C++ examples. A typical command line program might be structured as shown in Figure 2-1 Figure 2-1 illustrates that execution of the program shown starts at the beginning of the function main(). From main(), execution transfers to a function input_names(), which returns execution to the position immediately following the point where it was called in main(). The sort_names() function is then called from main(), and, after control returns to main(), the final function output_names() is called. Eventually, after output has been completed, execution returns again to main() and the program ends. Of course, different programs may have radically different functional structures, but they all start execution at the beginning of main(). The principal advantage of having a program broken up into functions is that you can write and test each piece separately. There is a further advantage in that functions written to perform a particular task can be re-used in other programs. The libraries that come with C++ provide a lot of standard functions that you can use in your programs. They can save you a great deal of work. You’ll see more about creating and using functions in Chapter 5.
38
Data, Variables, and Calculations
When a function is called, it starts at the beginning void input_names() { // ...
Execution starts with main()
return ;
int main() {
}
input_names(); sort_names(); void sort_names() { // ...
output_names(); return 0;
return ; } }
void output_names() { // ...
The return from main() goes back to the operating system
return ; The return from a function goes back to a point following where it was called
}
Figure 2-1
Try It Out
A Simple Program
A simple example can help you to understand the elements of a program a little better. Start by creating a new project — you can use the Ctrl+Shift+N key combination as a shortcut for this. When the New Project dialog box shown in Figure 2-2 appears, select Win32 as the project type and Win32 Console Application as the template. Name the project Ex2_01.
39
Data, Variables, and Calculations If you now click Application Settings on the left of this dialog box, you’ll see further options for a Win32 application displayed, as shown in Figure 2-4.
Figure 2-4
The default setting is a Console application that includes a file containing a default version of main(), but you’ll start from the most basic project structure, so choose Empty project from the set of additional options and click the Finish button. Now you have a project created, but it contains no files. You can see what the project contains from the Solution Explorer pane on the left of the Visual C++ 2005 main window, as shown in Figure 2-5. You’ll start by adding a new source file to the project, so right-click Source Files in the Solution Explorer pane and select the Add > New Item menu option. The Add New Item dialog box, similar to that shown in Figure 2-6, displays.
41
Chapter 2
Figure 2-5
Figure 2-6
42
Data, Variables, and Calculations Make sure the C++ File (.cpp) template is highlighted by clicking it and enter the file name, as shown in Figure 2-6. The file will automatically be given the extension .cpp, so you don’t have to enter the extension. There is no problem having the name of the file the same as the name of the project. The project file will have the extension .vcproj so that will differentiate it from the source file. Click on the Add button to create the file. You can then type the following code in the editor pane of the IDE window: // Ex2_01.cpp // A Simple Example of a Program #include using std::cout; using std::endl; int main() { int apples, oranges; int fruit; apples = 5; oranges = 6; fruit = apples + oranges;
// Declare two integer variables // ...then another one // Set initial values // Get the total fruit
cout << endl; // Start output on a new line cout << “Oranges are not the only fruit... “ << endl << “- and we have “ << fruit << “ fruits in all.”; cout << endl; // Output a new line character return 0;
// Exit the program
}
The preceding example is intended to illustrate some of the ways in which you can write C++ statements and is not a model of good programming style. Since the file is identified by its extension as a file containing C++ code, the keywords in the code that the editor recognizes will be colored to identify them. You will be able to see if you have entered Int where you should have entered int, because Int will not have the color used to highlight keywords in your source code. If you look at the Solution Explorer pane for your new project, you’ll see the newly created source file. Solution Explorer will always show all the files in a project. If you click on the Class View tab at the bottom of the Solution Explorer pane the Class View will be displayed. This consists of two panes, the upper pane showing global functions and macros within the project (and classes when you get to create a project involving classes) and the lower pane presently empty. The main() function will appear in the lower pane if you select Global Functions and Variables in the upper Class View pane; this is shown in Figure 2-7. I’ll consider what this means in more detail later, but essentially globals are functions and/or variables accessible from anywhere in the program.
43
Chapter 2
Figure 2-7
You have three ways to compile and link the program; you can select the Build Ex2_01 menu item from the Build menu, you can press the F7 function key, or you can select the appropriate toolbar button — you can identify what a toolbar button does by hovering the mouse cursor over it. Assuming the build operation was successful you can execute the program by pressing the Ctrl+F5 keys or by selecting Start Without Debugging from the Debug menu. You should get the following output in a command line window: Oranges are not the only fruit... - and we have 11 fruits in all. Press any key to continue . . .
The first two lines were produced by the program, and the last line indicates how you can end the execution and close the command line window.
Program Comments The first two lines in the program are comments. Comments are an important part of any program, but they’re not executable code — they are there simply to help the human reader. All comments are ignored by the compiler. On any line of code, two successive slashes // not contained within a text string (you’ll see what text strings are later) indicate that the rest of the line is a comment.
44
Data, Variables, and Calculations You can see that several lines of the program contain comments as well as program statements. You can also use an alternative form of comment bounded by /* and */. For example, the first line of the program could have been written: /*
Ex2_01.cpp
*/
The comment using // covers only the portion of the line following the two successive slashes, whereas the /*...*/ form defines whatever is enclosed between the /* and the */ as a comment and can span several lines. For example, you could write: /* Ex2_01.cpp A Simple Program Example */
All four lines are comments. If you want to highlight some particular comment lines, you can always embellish them with a frame of some description: /***************************** * Ex2-01.cpp * * A Simple Program Example * *****************************/
As a rule, you should always comment your programs comprehensively. The comments should be sufficient for another programmer or you at a later date to understand the purpose of any particular piece of code and how it works.
The #include Directive — Header Files Following the comments, you have an #include directive: #include
This is called a directive because it directs the compiler to do something — in this case to insert the contents of the file into the program before compilation. The file is called a header file because it’s usually brought in at the beginning of a program file. Actually is more properly referred to as a header because in ANSI standard C++ a header need not necessarily be contained in a file. However, I’ll use the term header file in this book because with Visual C++ 2005 all headers are stored in files. The header file contains definitions that are necessary for you to be able to use C++ input and output statements. If you didn’t include the contents of into the program, it wouldn’t compile because you use output statements in the program that depend on some of the definitions in this file. There are many different header files provided by Visual C++ that cover a wide range of capabilities. You’ll be seeing more of them as you progress through the language facilities. An #include statement is one of several preprocessor directives. The Visual C++ editor recognizes these and highlights them in blue in your edit window. Preprocessor directives are commands executed by the preprocessor phase of the compiler that executes before your code is compiled into object code, and preprocessor directives generally act on your source code in some way before it is compiled. They all start with the # character. I’ll be introducing other preprocessor directives as you need them.
45
Chapter 2 Namespaces and the Using Declaration As you saw in Chapter 1, the standard library is an extensive set of routines that have been written to carry many common tasks: for example, dealing with input and output, performing basic mathematical calculations. Since there are a very large number of these routines as well as other kinds of things that have names, it is quite possible that you might accidentally use the same name as one of names defined in the standard library for your own purposes. A namespace is a mechanism in C++ for avoiding problems that can arise when duplicate names are used in a program for different things, and it does this by associating a given set of names such as those from the standard library with a sort of family name, which is the namespace name. Every name that is defined in code that appears within a namespace also has the namespace name associated with it. All the standard library facilities for ISO/ANSI C++ are defined within a namespace with the name std, so every item from this standard library that you can access in your program has its own name, plus the namespace name, std, as a qualifier. The names cout and endl are defined within the standard library so their full names are std::cout and std::endl, and you saw these in action in Chapter 1. The two semicolons that separate the namespace name from the name of an entity form an operator called the scope resolution operator, and I’ll be discussing other uses for this operator later on in the book. Using the full names in the program will tend to make the code look a bit cluttered, so it would be nice to be able to use their simple names, unqualified by the namespace name, std. The two lines in our program that follow the #include directive for make this possible: using std::cout; using std::endl;
These are using declarations that tell the compiler that you intend to use the names cout and endl from the namespace std without specifying the namespace name. The compiler will now assume that wherever you use the name cout in the source file subsequent to the first using declaration, you mean the cout that is defined in the standard library. The name cout represents the standard output stream that by default corresponds to the command line and the name endl represents the newline character. You’ll learn more about namespaces, including how you define your own namespaces, a little later this chapter.
The main() Function The function main() in the example consists of the function header defining it as main() plus everything from the first opening curly brace ({) to the corresponding closing curly brace (}). The braces enclose the executable statements in the function, which are referred to collectively as the body of the function. As you’ll see, all functions consist of a header that defines (amongst other things) the function name, followed by the function body that consists of a number of program statements enclosed between a pair of braces. The body of a function may contain no statements at all, in which case it doesn’t do anything. A function that doesn’t do anything may seem somewhat superfluous, but when you’re writing a large program, you may map out the complete program structure in functions initially but omit the code for many of the functions leaving them with empty or minimal bodies. Doing this means that you can compile and execute the whole program with all its functions at any time and add detailed coding for the functions incrementally.
46
Data, Variables, and Calculations
Program Statements The program statements making up the function body of main() are each terminated with a semicolon. It’s the semicolon that marks the end of a statement, not the end of the line. Consequently a statement can be spread over several lines when this makes the code easier to follow and several statements can appear in a single line. The program statement is the basic unit in defining what a program does. This is a bit like a sentence in a paragraph of text, where each sentence stands by itself in expressing an action or an idea, but relates to and combines with the other sentences in the paragraph in expressing a more general idea. A statement is a self-contained definition of an action that the computer is to carry out, but that can be combined with other statements to define a more complex action or calculation. The action of a function is always expressed by a number of statements, each ending with a semicolon. Take a quick look at each of the statements in the example just written, just to get a general feel for how it works. I will discuss each type of statement more fully later in this chapter. The first statement in the body of the main() function is: int apples, oranges;
// Declare two integer variables
This statement declares two variables, apples and oranges. A variable is just a named bit of computer memory that you can use to store data, and a statement that introduces the names of one or more variables is called a variable declaration. The keyword int in the preceding statement indicates that the variables with the names apples and oranges are to store values that are whole numbers, or integers. Whenever you introduce the name of a variable into a program, you always specify what kind of data it will store, and this is called the type of the variable. The next statement declares another integer variable, fruit: int fruit;
// ...then another one
While you can declare several variables in the same statement, as you did in the preceding statement for apples and oranges, it is generally a good idea to declare each variable in a separate statement on its own line as this enables you to comment them individually to explain how you intend to use them. The next line in the example is: apples = 5; oranges = 6;
// Set initial values
This line contains two statements, each terminated by a semicolon. I put this here just to demonstrate that you can put more than one statement in a line. While it isn’t obligatory, it’s generally good programming practice to write only one statement on a line as it makes the code easier to understand. Good programming practice is about adopting approaches to coding that make your code easy to follow, and minimize the likelihood of errors. The two statements in the preceding line store the values 5 and 6 in the variables apples and oranges, respectively. These statements are called assignment statements because they assign a new value to a variable and the = is the assignment operator. The next statement is: fruit = apples + oranges;
// Get the total fruit
47
Chapter 2 This is also an assignment statement but is a little different because you have an arithmetic expression to the right of the assignment operator. This statement adds together the values stored in the variables apples and oranges and stores the result in the variable fruit. The next three statements are: cout << endl; // Start output on a new line cout << “Oranges are not the only fruit... “ << endl << “- and we have “ << fruit << “ fruits in all.”; cout << endl; // Start output on a new line
These are all output statements. The first statement is the first line here, and it sends a newline character, denoted by the word endl, to the command line on the screen. In C++, a source of input or a destination for output is referred to as a stream. The name cout specifies the “standard” output stream, and the operator << indicates that what appears to the right of the operator is to be sent to the output stream, cout. The << operator “points” in the direction that the data flows — from the variable or string that appears on the right of the operator to the output destination on the left. Thus in the first statement the value represented by the name endl — which represents a newline character — is sent to the stream identified by the name cout — and data transferred to cout is written to the command the command line. The meaning of the name cout and the operator << are defined in the standard library header file , which you added to the program code by means of the #include directive at the beginning of the program. cout is a name in the standard library and therefore is within the namespace std. Without the using directive it would not be recognized unless you used its fully qualified name, which is std::cout, as I mentioned earlier. Because cout has been defined to represent the standard output stream, you shouldn’t use the name cout for other purposes so you can’t use it as the name of a variable in your program for example. Obviously, using the same name for different things is likely to cause confusion. The second output statement of the three is spread over two lines: cout << “Oranges are not the only fruit... “ << endl << “- and we have “ << fruit << “ fruits in all.”;
As I said earlier, you can spread each statement in a program over as many lines as you wish if it helps to make the code clearer. The end of a statement is always signaled by a semicolon, not the end of a line. Successive lines are read and combined into a single statement by the compiler until it finds the semicolon that defines the end of the statement. Of course, this means that if you forget to put a semicolon at the end of a statement, the compiler will assume the next line is part of the same statement and join them together. This usually results in something the compiler cannot understand, so you’ll get an error message. The statement sends the text string “Oranges are not the only fruit... “ to the command line, followed by another newline character (endl), then another text string, “- and we have “, followed by the value stored in the variable fruit, then finally another text string, “ fruits in all.”. There is no problem stringing together a sequence of things that you want to output in this way. The statement executes from left to right, with each item being sent to cout in turn. Note that each item to be sent to cout is preceded by its own << operator.
48
Data, Variables, and Calculations The third and last output statement just sends another newline character to the screen, and the three statements produce the output from the program that you see. The last statement in the program is: return 0;
// Exit the program
This terminates execution of the main() function which stops execution of the program. Control returns to the operating system. I’ll be discussing all of these statements in more detail later on. The statements in a program are executed in the sequence in which they are written, unless a statement specifically causes the natural sequence to be altered. In Chapter 3, you’ll look at statements that alter the sequence of execution.
Whitespace Whitespace is the term used in C++ to describe blanks, tabs, newline characters, form feed characters, and comments. Whitespace serves to separate one part of a statement from another and enables the compiler to identify where one element in a statement, such as int, ends and the next element begins. Otherwise, whitespace is ignored and has no effect. Look at this statement for example: int fruit;
// ...then another one
There must be at least one whitespace character (usually a space) between int and fruit for the compiler to be able to distinguish them but if you add more whitespace characters they will be ignore. The content of the line following the semicolon is all whitespace and is therefore ignored. On the other hand, look at this statement: fruit = apples + oranges;
// Get the total fruit
No whitespace characters are necessary between fruit and =, or between = and apples, although you are free to include some if you wish. This is because the = is not alphabetic or numeric, so the compiler can separate it from its surroundings. Similarly, no whitespace characters are necessary on either side of the + sign but you can include some if you want to aid the readability of your code. As I said, apart from its use as a separator between elements in a statement that might otherwise be confused, whitespace is ignored by the compiler (except, of course, in a string of characters between quotes). You can therefore include as much whitespace as you like to make your program more readable, as you did when you spread an output statement in the last example over several lines. Remember that in C++ the end of a statement is wherever the semicolon occurs.
Statement Blocks You can enclose several statements between a pair of braces, in which case they become a block, or a compound statement. The body of a function is an example of a block. Such a compound statement can be thought of as a single statement (as you’ll see when you look at the decision-making possibilities in
49
Chapter 2 C++ in Chapter 3). In fact, wherever you can put a single statement in C++, you could equally well put a block of statements between braces. As a consequence, blocks can be placed inside other blocks. In fact, blocks can be nested, one within another, to any depth.
A statement block also has important effects on variables, but I will defer discussion of this until later in this chapter when I discuss something called variable scope.
Automatically Generated Console Programs In the last example, you opted to produce the project as an empty project with no source files, and then you added the source file subsequently. If you just allow the Application Wizard to generate the project as you did in Chapter 1, the project will contain several files, and you should explore their contents in a little more depth. Create a new Win32 console project with the name Ex2_01A and this time just allow the Application Wizard to finish without choosing to set any of the options in the Application Settings dialog. The project will have three files containing code: the Ex2_01A.cpp and stdafx.cpp source files, and the stdafx.h header file. This is to provide for basic capability that you might need in a console program and represents a working program at it stands, which does nothing. If you have a project open, you can close it by selecting the File > Close Solution item on the main menu. You can create a new project with an existing project open, in which case the old project will be closed automatically unless you elect to add it to the same solution. First of all, the contents of Ex2_01A.cpp will be: #include “stdafx.h” int _tmain(int argc, _TCHAR* argv[]) { return 0; }
This is decidedly different from the previous example. There is an #include directive for the stdafx.h header file that was not in the previous version, and the function where execution starts is called _tmain(), not main(). The Application Wizard has generated the stdafx.h header file as part of the project, and if you take a look at the code in there, you’ll see there are two further #include directives for standard library header files stdio.h and tchar.h. stdio.h is the old-style header for standard I/O that were used before the current ISO/ANSI standard for C++; this covers essentially the same functionality as the header. tchar.h is a Microsoft-specific header file defining text functions. The idea is that stdafx.h should define a set of standard system include files for your project you would add #include directives for any other system headers that you need in this file. While you are learning ISO/ANSI C++, you won’t be using either of the headers that appear in stdafx.h, which is one reason for not using the default file generation capability provided by the Application Wizard. As I already explained, Visual C++ 2005 supports wmain() as an alternative to main() when you are writing a program that’s using Unicode characters — wmain() being a Microsoft-specific that is not part of ISO/ANSI C++. In support of that, the tchar.h header defines the name _tmain so that it will
50
Data, Variables, and Calculations normally replaced by main but will be replaced by wmain if the symbol _UNICODE is defined. Thus to identify a program as using UNICODE you would add the following statement to the beginning of the stdafx.h header file: #define _UNICODE
Now that we’ve explained all that, we’ll stick to plain old main() for our ISO/ANSI C++ examples.
Defining Variables A fundamental objective in all computer programs is to manipulate some data and get some answers. An essential element in this process is having a piece of memory that you can call your own, that you can refer to using a meaningful name, and where you can store an item of data. Each piece of memory so specified is called a variable. As you already know, each variable will store a particular kind of data, and the type of data that can be stored is fixed when you define the variable in your program. One variable might store whole numbers (that is, integers), in which case you couldn’t use it to store numbers with fractional values. The value that each variable contains at any point is determined by the statements in your program, and of course, its value will usually change many times as the program calculation progresses. The next section looks first at the rules for naming a variable when you introduce it into a program.
Naming Variables The name you give to a variable is called an identifier, or more conveniently a variable name. Variable names can include the letters A-z (uppercase or lowercase), the digits 0-9 and the underscore character. No other characters are allowed, and if you happen to use some other character, you will typically get an error message when you try to compile the program. Variable names must also begin with either a letter or an underscore. Names are usually chosen to indicate the kind of information to be stored. Because variable names in Visual C++ 2005 can be up to 2048 characters long, you have a reasonable amount of flexibility in what you call your variables. In fact, as well as variables, there are quite a few other things that have names in C++ and they too can have names of up to 2048 characters, with the same definition rules as a variable name. Using names of the maximum length allowed can make your programs a little difficult to read, and unless you have amazing keyboard skills, they are the very devil to type in. A more serious consideration is that not all compilers support such long names. If you anticipate compiling your code in other environments, it’s a good idea to limit names to a maximum of 31 characters; this will usually be adequate for devising meaningful names and will avoid problems of compiler name length constraints in most instances. Although you can use variable names that begin with an underscore, for example _this and _that, this is best avoided because of potential clashes with standard system variables that have the same form. You should also avoid using names starting with a double underscore for the same reason.
51
Chapter 2 Examples of good variable names are: ❑
price
❑
discount
❑
pShape
❑
value_
❑
COUNT
8_Ball, 7Up, and 6_pack are not legal. Neither is Hash! or Mary-Ann. This last example is a common mistake, although Mary_Ann with an underscore in place of the hyphen would be quite acceptable. Of course, Mary Ann would not be, because blanks are not allowed in variable names. Note that the variable names republican and Republican are quite different, as names are case-sensitive so upper- and low-
ercase letters are differentiated. Of course, whitespace characters in general cannot appear within a name, and if you inadvertently include whitespace characters, you will have two or more names instead of one, which will usually cause the compiler to complain. A convention that is often adopted in C++ is to reserve names beginning with a capital letter for naming classes and use names beginning with a lowercase letter for variables. I’ll discuss classes in Chapter 8.
Keywords in C++ There are reserved words in C++, also called keywords, that have special significance within the language. They will be highlighted with a particular color by the Visual C++ 2005 editor as you enter your program — in my system the default color is blue. If a keyword you type does not appear highlighted, then you have entered the keyword incorrectly. Remember that keywords, like the rest of the C++ language, are case-sensitive. For example, the program that you entered earlier in the chapter contained the keywords int and return; if you write Int or Return, these are not keywords and therefore will not be recognized as such. You will see many more as you progress through the book. You must ensure that the names you choose for entities in your program, such as variables, are not the same as any of the keywords in C++. A complete list of the keywords used in Visual C++ 2005 appears in Appendix A.
Declaring Variables As you saw earlier, a variable declaration is a program statement that specifies the name of a variable of a given type. For example: int value;
This declares a variable with the name value that can store integers. The type of data that can be stored in the variable value is specified by the keyword int, so you can only use value to store data of type int. Because int is a keyword, you can’t use int as a name for one of your variables. Note that a variable declaration always ends with a semicolon.
52
Data, Variables, and Calculations A single declaration can specify the names of several variables but, as I have said, it is generally better to declare variables in individual statements, one per line. I’ll deviate from this from time to time in this book, but only in the interest of not spreading code over too many pages. In order to store data (for example, the value of an integer), you not only need to have defined the name of the variable; you also need to have associated a piece of the computer’s memory with the variable name. This process is called variable definition. In C++, a variable declaration is also a definition (except in a few special cases, which we shall come across during the book). In the course of a single statement, we introduce the variable name, and also tie it to an appropriately sized piece of memory. So, the statement int value;
is both a declaration and a definition. You use the variable name value that you have declared, to access the piece of the computer’s memory that you have defined and that can store a single value of type int. You use the term declaration when you introduce a name into your program, with information on what the name will be used for. The term definition refers to the allotment of computer memory to the name. In the case of variables, you can declare and define in a single statement, as in the preceding line. The reason for this apparently pedantic differentiation between a declaration and a definition is that you will meet statements that are declarations but not definitions. You must declare a variable at some point between the beginning of your program and when the variable is used for the first time. In C++, it is good practice to declare variables close to their first point of use.
Initial Values for Variables When you declare a variable, you can also assign an initial value to it. A variable declaration that assigns an initial value to a variable is called an initialization. To initialize a variable when you declare it, you just need to write an equals sign followed by the initializing value after the variable name. You can write the following statements to give each of the variables an initial value: int value = 0; int count = 10; int number = 5;
In this case, value will have the value 0, count will have the value 10, and number will have the value 5. There is another way of writing the initial value for a variable in C++ called functional notation. Instead of an equals sign and the value, you can simply write the value in parentheses following the variable name. So you could rewrite the previous declarations as: int value(0); int count(10); int number(5);
53
Chapter 2 If you don’t supply an initial value for a variable, it will usually contain whatever garbage was left in the memory location it occupies by the previous program you ran (there is an exception to this that you will meet later in this chapter). Wherever possible, you should initialize your variables when you declare them. If your variables start out with known values, it makes it easier to work out what is happening when things go wrong. And one thing you can be sure of — things will go wrong.
Fundamental Data Types The sort of information that a variable can hold is determined by its data type. All data and variables in your program must be of some defined type. ISO/ANSI standard C++ provides you with a range of fundamental data types, specified by particular keywords. Fundamental data types are so called because they store values of types that represent fundamental data in your computer, essentially numerical values, which also includes characters because a character is represented by a numerical character code. You have already seen the keyword int for defining integer variables. C++/CLI also defines fundamental data types that are not part of ISO/ANSI C++, and I’ll go into those a little later in this chapter. As part of the object-oriented aspects of the language, you can also create your own data types, as you’ll see later, and of course the various libraries that you have at your disposal with Visual C++ 2005 also define further data types. For the moment, explore the elementary numerical data types that ISO/ANSI C++ provides. The fundamental types fall into three categories, types that store integers, types that store non-integral values — which are called floating-point types, and the void type that specifies an empty set of values or no type.
Integer Variables As I have said, integer variables are variables that can have only values that are whole numbers. The number of players in a football team is an integer, at least at the beginning of the game. You already know that you can declare integer variables using the keyword int. Variables of type int occupy 4 bytes in memory and can store both positive and negative integer values. The upper and lower limits for the values of a variable of type int correspond to the maximum and minimum signed binary numbers, which can be represented by 32 bits. The upper limit for a variable of type int is 231–1 which is 2,147,483,647, and the lower limit is –(231), which is –2,147,483,648. Here’s an example of defining a variable of type int: int toeCount = 10;
In Visual C++ 2005, the keyword short also defines an integer variable, this time occupying two bytes. The keyword short is equivalent to short int, and you could define two variables of type short with the following statements: short feetPerPerson = 2; short int feetPerYard = 3;
Both variables are of the same type here because short means exactly the same as short int. I used both forms of the type name to show them in use, but it would be best to stick to one representation of the type in your programs and short is used most often.
54
Data, Variables, and Calculations C++ also provides another integer type, long, which can also be written as long int. Here’s how you declare variables of type long: long bigNumber = 1000000L; long largeValue = 0L;
These statements declare the variables bigNumber and largeValue with initial values 1000000 and 0, respectively. The letter L appended to the end of the literals specifies that they are integers of type long. You can also use the small letter l for the same purpose, but it has the disadvantage that it is easily confused with the numeral 1. Integer literals without an L appended are of type int. You must not include commas when writing large numeric values in a program. In text you might write the number 12,345, but in your program code you must write this as 12345. Integer variables declared as long in Visual C++ 2005 occupy 4 bytes and can have values from 2,147,483,648 to 2,147,483,647. This is the same range as for variables declared as int. With other C++ compilers, variables of type long (which is the same as type long int) may not be the same as type int, so if you expect your programs to be compiled in other environments, don’t assume that long and int are equivalent. For truly portable code, you should not even assume that an int is 4 bytes (for example, under older 16-bit versions of Visual C++ a variable of type int was 2 bytes).
Character Data Types The char data type serves a dual purpose. It specifies a one-byte variable that you can use to store integers within a given range or to store the code for a single ASCII character, which is the American Standard Code for Information Interchange. The codes in the ASCII character set appear in Appendix B. You can declare a char variable with this statement: char letter = ‘A’;
This declares the variable with the name letter and initializes it with the constant ‘A’. Note that you specify a value that is a single character between single quotes, rather than the double quotes used previously for defining a string of characters to be displayed. A string of characters is a series of values of type char that are grouped together into a single entity called an array. I’ll discuss arrays and how strings are handled in C++ in Chapter 4. Because the character ‘A’ is represented in ASCII by the decimal value 65, you could have written the statement as: char letter = 65;
// Equivalent to A
This produces the same result as the previous statement. The range of integers that can be stored in a variable of type char with Visual C++ is from –128 to 127. Note that the ISO/ANSI C++ standard does not require that type char should represent signed 1-byte integers. It is the compiler implementer’s choice as to whether type char represents signed integers in the range -128 to +127 or unsigned integers in the range 0 to 255. You need to keep this in mind if you are porting your C++ code to a different environment.
55
Chapter 2 The type wchar_t is so called because it is a wide character type, and variables of this type store 2-byte character codes with values in the range from 0 to 65,535. Here’s an example of defining a variable of type wchar_t: wchar_t letter = L’Z’;
// A variable storing a 16-bit character code
This defines a variable, letter, that is initialized with the 16-bit code for the letter Z. The L preceding the character constant, ‘Z’, tells the compiler that this is a 16-bit character code value. You can also use hexadecimal constants to initialize char variables (and other integer types), and it is obviously going to be easier to use this notation when character codes are available as hexadecimal values. A hexadecimal number is written using the standard representation for hexadecimal digits: 0 to 9, and A to F (or a to f) for digits with values from 10 to 15. It’s also preceded by 0x (or 0X) to distinguish it from a decimal value. Thus, to get exactly the same result again, you could rewrite the last statement as follows: char letter = 0x41;
// Equivalent to A
Don’t write decimal integer values with a leading zero. The compiler will interpret such values as octal (base 8), so a value written as 065 will be equivalent to 53 in normal decimal notation. Also take note that Windows XP provides a Character Map utility that enables you to locate characters from any of the fonts available to Windows. It will show the character code in hexadecimal and tell you the keystroke to use for entering the character. You’ll find the Character Map utility if you click the Start button and look in the System Tools folder that is within the Accessories folder.
Integer Type Modifiers Variables of the integral types char, int, short, or long store signed integer values by default, so you can use these types to store either positive or negative values. This is because these types are assumed to have the default type modifier signed. So, wherever you wrote int or long you could have written signed int or signed long, respectively. You can also use the signed keyword by itself to specify the type of a variable, in which case it means signed int. For example: signed value = -5;
// Equivalent to signed int
This usage is not particularly common, and I prefer to use int, which makes it more obvious what is meant. The range of values that can be stored in a variable of type char is from –128 to +127, which is the same as the range of values you can store in a variable of type signed char. In spite of this, type char and type signed char are different types, so you should not make the mistake of assuming they are the same. If you are sure that you don’t need to store negative values in a variable (for example, if you were recording the number of miles you drive in a week), you can specify a variable as unsigned: unsigned long mileage = 0UL;
56
Data, Variables, and Calculations Here, the minimum value that can be stored in the variable mileage is zero, and the maximum value is 4,294,967,295 (that’s 232–1). Compare this to the range of –2,147,483,648 to 2,147,483,647 for a signed long. The bit that is used in a signed variable to determine the sign of the value is used in an unsigned variable as part of the numeric value instead. Consequently, an unsigned variable has a larger range of positive values, but it can’t represent a negative value. Note how a U (or u) is appended to unsigned constants. In the preceding example, I also have L appended to indicate that the constant is long. You can use either upper- or lowercase for U and L, and the sequence is unimportant. However, it’s a good idea to adopt a consistent way of specifying such values. You can also use unsigned by itself as the type specification for a variable, in which case you are specifying the variable to be of type unsigned int. Remember, both signed and unsigned are keywords, so you can’t use them as variable names.
The Boolean Type Boolean variables are variables can have only two values: true and false. The type for a logical variable is bool, named after George Boole, who developed Boolean algebra, and type bool is regarded as an integer type. Boolean variables are also referred to as logical variables. Variables of type bool are used to store the results of tests that can be either true or false, such as whether one value is equal to another. You could declare the name of a variable of type bool with the statement: bool testResult;
Of course, you can also initialize variables of type bool when you declare them: bool colorIsRed = true;
You will find that the values TRUE and FALSE are used quite extensively with variables of numeric type, and particularly of type int. This is a hangover from the time before variables of type bool were implemented in C++ when variables of type int were typically used to represent logical values. In this case a zero value is treated as false and a non-zero value as true. The symbols TRUE and FALSE are still used within the MFC where they represent a non-zero integer value and 0, respectively. Note that TRUE and FALSE — written with capital letters — are not keywords in C++; they are just symbols defined within the MFC. Note also that TRUE and FALSE are not legal bool values, so don’t confuse true with TRUE.
Floating-Point Types Values that aren’t integral are stored as floating-point numbers. A floating-point number can be expressed as a decimal value such as 112.5, or with an exponent such as 1.125E2 where the decimal part is multiplied by the power of 10 specified after the E (for Exponent). Our example is, therefore, 1.125×102, which is 112.5.
A floating-point constant must contain a decimal point, or an exponent, or both. If you write a numerical value with neither, you have an integer.
57
Chapter 2 You can specify a floating-point variable using the keyword double, as in this statement: double in_to_mm = 25.4;
A variable of type double occupies 8 bytes of memory and stores values accurate to approximately 15 decimal digits. The range of values stored is much wider than that indicated by the 15 digits accuracy, being from 1.7×10-308 to 1.7×10308, positive and negative. If you don’t need 15 digits precision, and you don’t need the massive range of values provided by double variables, you can opt to use the keyword float to declare floating-point variables occupying 4 bytes. For example: float pi = 3.14159f;
This statement defines a variable pi with the initial value 3.14159. The f at the end of the constant specifies that it is of type float. Without the f, the constant would have been of type double. Variables that you declare as float have approximately 7 decimal digits of precision and can have values from 3.4×10-38 to 3.4×1038, positive and negative. The ISO/ANSI standard for C++ also defines the long double floating-point type, which in Visual C++ 2005 is implemented with the same range and precision as type double. With some compilers long double corresponds to a 16-byte floating-point value with a much greater range and precision compared than type double.
Fundamental Types in ISO/ANSI C++ The following table contains a summary of all the fundamental types in ISO/ANSI C++ and the range of values supported for these in Visual C++ 2005:
58
Type
Size in Bytes
Range of Values
bool
1
true or false
char
1
By default the same as type signed char: –128 to +127Optionally you can make char the same range as type unsigned char.
signed char
1
–128 to +127
unsigned char
1
0 to 255
wchar_t
2
0 to 65,535
short
2
–32,768 to +32,767
unsigned short
2
0 to 65,535
int
4
–2,147,483,648 to 2,147,483,647
unsigned int
4
0 to 4,294,967,295
long
4
–2,147,483,648 to 2,147,483,647
unsigned long
4
0 to 4,294,967,295
Data, Variables, and Calculations Type
Size in Bytes
Range of Values
float
4
±3.4x10±38 with approximately 7 digits accuracy
double
8
±1.7x10±308 with approximately 15 digits accuracy
long double
8
±1.7x10±308 with approximately 15 digits accuracy
Literals I have already used lots of explicit values to initialize variables and in C++, fixed values of any kind are referred to as literals. A literal is a value of a specific type so values such as 23, 3.14159, 9.5f, and true are examples of literals of type int, type double, type float, and type bool, respectively. The literal “Samuel Beckett” is an example of a literal that is a string, but I’ll defer discussion of exactly what type this is until Chapter 4. Here’s a summary of how you write literals of various types: Type
Examples of Literals
char, signed char, or unsigned char
‘A’, ‘Z’, ‘8’, ‘*’
wchar_t
L’A’, L’Z’, L’8’, L’*’
int
-77, 65, 12345, 0x9FE
unsigned int
10U, 64000U
long
-77L, 65L, 12345L
unsigned long
5UL, 999999999UL
float
3.14f, 34.506f
double
1.414, 2.71828
long double
1.414L, 2.71828L
bool
true, false
You can’t specify a literal to be of type short or unsigned short, but the compiler will accept initial values that are literals of type int for variables of these types provided the value of the literal is within the range of the variable type. You will often need to use literals in calculations within a program, for example, conversion values such as 12 for feet into inches or 25.4 for inches to millimeters, or a string to specify an error message. However, you should avoid using numeric literals within programs explicitly where their significance is not obvious. It is not necessarily apparent to everyone that when you use the value 2.54, it is the number of centimeters in an inch. It is better to declare a variable with a fixed value corresponding to your literal instead — you might name the variable inchesToCentimeters, for example. Then wherever you use inchesToCentimeters in your code, it will be quite obvious what it is. You will see how to fix the value of a variable a little later on in this chapter.
59
Chapter 2
Defining Synonyms for Data Types The typedef keyword enables you to define your own type name for an existing type. Using typedef, you could define the type name BigOnes as equivalent to the standard long int type with the declaration: typedef long int BigOnes;
// Defining BigOnes as a type name
This defines BigOnes as an alternative type specifier for long int, so you could declare a variable mynum as long int with the declaration: BigOnes mynum = 0L;
// Define a long int variable
There’s no difference between this declaration and the one using the built-in type name. You could equally well use: long int mynum = 0L;
// Define a long int variable
for exactly the same result. In fact, if you define your own type name such as BigOnes, you can use both type specifiers within the same program for declaring different variables that will end up as having the same type. Since typedef only defines a synonym for an existing type, it may appear to be a bit superficial, but it is not at all. You’ll see later that it fulfills a very useful role in enabling you to simplify more complex declarations by defining a single name that represents a somewhat convoluted type specification, and this can make your code much more readable.
Variables with Specific Sets of Values You will sometimes be faced with the need for variables that have a limited set of possible values that can be usefully referred to by labels — the days of the week, for example, or months of the year. There is a specific facility in C++ to handle this situation, called an enumeration. Take one of the examples I have just mentioned — a variable that can assume values corresponding to days of the week. You can define this as follows: enum Week{Mon, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek;
This declares an enumeration type with the name Week and the variable thisWeek, which is an instance of the enumeration type Week that can assume only the constant values specified between the braces. If you try to assign to thisWeek anything other than one of the set of values specified, it will cause an error. The symbolic names listed between the braces are known as enumerators. In fact, each of the names of the days will be automatically defined as representing a fixed integer value. The first name in the list, Mon, will have the value 0, Tues will be 1, and so on. You could assign one of the enumeration constants as the value of the variable thisWeek like this: thisWeek = Thurs;
60
Data, Variables, and Calculations Note that you do not need to qualify the enumeration constant with the name of the enumeration. The value of thisWeek will be 3 because the symbolic constants that an enumeration defines are assigned values of type int by default in sequence starting with 0. By default, each successive enumerator is one larger than the value of the previous one, but if you would prefer the implicit numbering to start at a different value, you can just write: enum Week {Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek;
Now the enumeration constants will be equivalent to 1 through 7. The enumerators don’t even need to have unique values. You could define Mon and Tues as both having the value 1, for example, with the statement: enum Week {Mon = 1, Tues = 1, Wed, Thurs, Fri, Sat, Sun} thisWeek;
As the type of the variable thisWeek is type int, it will occupy four bytes, as will all variables that are of an enumeration type. Having defined the form of an enumeration, you can define another variable thus: enum Week nextWeek;
This defines a variable nextWeek as an enumeration that can assume the values previously specified. You can also omit the enum keyword in declaring a variable, so, instead of the previous statement, you could write: Week next_week;
If you wish, you can assign specific values to all the enumerators. For example, you could define this enumeration: enum Punctuation {Comma = ‘,’, Exclamation = ‘!’, Question = ‘?’} things;
Here you have defined the possible values for the variable things as the numerical equivalents of the appropriate symbols. If you look in the ASCII table in Appendix B, you will see that the symbols are 44, 33 and 63, respectively, in decimal. As you can see, the values assigned don’t have to be in ascending order. If you don’t specify all the values explicitly, each enumerator will be assigned a value incrementing by 1 from the last specified value, as in our second Week example. You can omit the enumeration type if you don’t need to define other variables of this type later. For example: enum {Mon, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek, nextWeek, lastWeek;
Here you have three variables declared that can assume values from Mon to Sun. Because the enumeration type is not specified, you cannot refer to it. Note that you cannot define other variables for this enumeration at all, because you would not be permitted to repeat the definition. Doing so would imply that you were redefining values for Mon to Sun, and this isn’t allowed.
61
Chapter 2
Specifying the Type for Enumeration Constants Enumeration constants are type int by default, but you can also choose to specify the type explicitly by adding a colon and the type name for the constants following the enumeration type name in the declaration. You can specify the type for the enumeration constants as any of the signed or unsigned integer types: short, int, long, and char, or as type bool. Thus, you could define the enumeration representing days of the week as: enum Week: char{ Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
Here the enumeration constants will be of type char with the first constant as 0. However, with the constants as type char you might prefer to initialize them explicitly, like this: enum Week : char{ Monday=’M’, Tuesday=’T’, Wednesday=’W’, Thursday=’T’, Friday=’F’, Saturday=’S’, Sunday=’S’};
Now the values for the constants reflect a little more what they represent, although they do not distinguish between Thursday and Tuesday or between Saturday and Sunday. There is no problem with having duplicate values for the constants, but of course, all the names must be unique. Here’s an example of an enumeration with bool constants: enum State : bool { On = true, Off};
Because On has the initial value true, Off will be false. If there were subsequent enumerations constants specified, they would alternate in value by default.
Basic Input/Output Operations Here, you will only look at enough of native C++ input and output to get you through learning about C++. It’s not that it’s difficult — quite the opposite in fact — but for Windows programming you won’t need it at all. C++ input/output revolves around the notion of a data stream, where you can insert data into an output stream or extract data from an input stream. You have already seen that the ISO/ANSI C++ standard output stream to the command line on the screen is referred to as cout. The complementary input stream from the keyboard is referred to as cin.
Input from the Keyboard You obtain input from the keyboard through the stream cin, using the extractor operator for a stream >>. To read two integer values from the keyboard into integer variables num1 and num2, you can write this statement: cin >> num1 >> num2;
The extraction operator, >>, “points” in the direction that data flows — in this case, from cin to each of the two variables in turn. Any leading whitespace is skipped, and the first integer value you key in is read into num1. This is because the input statement executes from left to right. Any whitespace following
62
Data, Variables, and Calculations num1 is ignored and the second integer value that you enter is read into num2. There has to be some
whitespace between successive values though, so that they can be differentiated. The stream input operation ends when you press the Enter key, and execution then continues with the next statement. Of course, errors can arise if you key in the wrong data, but I will assume that you always get it right! Floating-point values are read from the keyboard in exactly the same way as integers and, of course, you can mix the two. The stream input and operations automatically deal with variables and data of any of the fundamental types. For example, in the statements int num1 = 0, num2 = 0; double factor = 0.0; cin >> num1 >> factor >> num2;
the last line will read an integer into num1, then a floating-point value into factor and, finally, an integer into num2.
Output to the Command Line You have already seen output to the command line, but I want to revisit it anyway. Writing information to the display operates in a complementary fashion to input. As you have seen, the stream is called cout, and you use the insertion operator, << to transfer data to the output stream. This operator also “points” in the direction of data movement. You have already used this operator to output a text string between quotes. I can demonstrate the process of outputting the value of a variable with a simple program.
Try It Out
Output to the Command Line
I’ll assume that you’ve got the hang of creating a new empty project adding a new source file to the project and building it into an executable. Here’s the code that you need to put in the source file once you have created the Ex2_02 project: // Ex2_02.cpp // Exercising output #include using std::cout; using std::endl; int main() { int num1 = 1234, num2 = 5678; cout << endl; cout << num1 << num2; cout << endl; return 0; }
// // // //
Start on a new line Output two values End on a new line Exit program
How It Works The first statement in the body of main() declares and initializes two integer variables, num1 and num2. This is followed by two output statements, the first of which moves the screen cursor position to a new
63
Chapter 2 line. Because output statements execute from left to right, the second output statement displays the value of num1 followed by the value of num2. When you compile and execute this, you will get the output: 12345678
The output is correct, but it’s not exactly helpful. You really need the two output values to be separated by at least one space. The default for stream output is to just output the digits in the output value, which doesn’t provide for spacing successive output values out nicely so they can be differentiated. As it is, you have no way to tell where the first number ends and the second number begins.
Formatting the Output You can fix the problem of there being no spaces between items of data quite easily, though, just by outputting a space between the two values. You can do this by replacing the following line in your original program: cout << num1 << num2;
// Output two values
Just substitute the statement: cout << num1 << ‘ ‘ << num2;
// Output two values
Of course, if you had several rows of output that you wanted to align in columns, you would need some extra capability because you do not know how many digits there will be in each value. You can take care of this situation by using what is called a manipulator. A manipulator modifies the way in which data output to (or input from) a stream is handled. Manipulators are defined in the header file , so you need to add an #include directive for it. The manipulator that you’ll use is setw(n), which will output the value that follows right-justified in a field n spaces wide, so setw(6) causes the next output value to be presented in a field with a width of six spaces. Let’s see it working.
Try It Out
Using Manipulators
To get something more like the output you want, you can change the program to the following: // Ex2_03.cpp // Exercising output #include #include using std::cout; using std::endl; using std::setw; int main() { int num1 = 1234, num2 = 5678;
64
Data, Variables, and Calculations cout << endl; cout << setw(6) << num1 << setw(6) << num2; cout << endl; return 0;
// // // //
Start on a new line Output two values Start on a new line Exit program
}
How It Works The changes from the last example are the addition of the #include directive for the header file, the addition of a using declaration for the setw name from the std namespace, and the insertion of the setw() manipulator in the output stream preceding each value so that the output values are presented in a field six characters wide. Now you get nice neat output where you can actually separate the two values: 1234
5678
Note that the setw() manipulator works only for the single output value immediately following its insertion into the stream. You have to insert the manipulator into the stream immediately preceding each value that you want to output within a given field width. If you put only one setw(), it would apply to the first value to be output after it was inserted. Any following value would be output in the default manner. You could try this out by deleting the second setw(6) and its insertion operator in the example.
Escape Sequences When you write a character string between double quotes, you can include special characters called escape sequences in the string They are called escape sequences because they allow characters to be included in a string that otherwise could not be represented by escaping from the default interpretation of the characters. An escape sequence starts with a backslash character, \, and the backslash character cues the compiler to interpret the character that follows in a special way. For example, a tab character is written as \t, so the t is understood by the compiler to represent a tab in the string, not the letter t. Look at these two output statements cout << endl << “This is output.”; cout << endl << “\tThis is output after a tab.”;
They will produce these lines: This is output. This is output after a tab.
The \t in the second output statement causes the output text to be indented to the first tab position. In fact, instead of using endl you could use the escape sequence for the newline character, \n, in each string, so you could rewrite the preceding statements as follows: cout << “\nThis is output.”; cout << “\n\tThis is output after a tab.”;
65
Chapter 2 Here are some escape sequences that may be particularly useful: Escape Sequence
What It Does
\a
sounds a beep
\n
newline
\’
single quote
\\
backslash
\b
backspace
\t
tab
\”
double quote
\?
question mark
Obviously, if you want to be able to include a backslash or a double quote as a character to appear in a string, you must use the appropriate escape sequences to represent them. Otherwise, the backslash would be interpreted as the start of another escape sequence, and the double quote would indicate the end of the character string. You can also use characters specified by escape sequences in the initialization of variables of type char. For example: char Tab = ‘\t’;
// Initialize with tab character
Because a character literal is delimited by single quote characters, you must use an escape sequence to specify a character literal that is a single quote, thus ‘\’’.
Try It Out
Using Escape Sequences
Here’s a program that uses some of the escape sequences in the table in the previous section: // Ex2_04.cpp // Using escape sequences #include #include using std::cout; int main() { char newline = ‘\n’; // Newline escape sequence cout << newline; // Start on a new line cout << “\”We\’ll make our escapes in sequence\”, he said.”; cout << “\n\tThe program\’s over, it\’s time take make a beep beep.\a\a”; cout << newline; // Start on a new line return 0; // Exit program }
66
Data, Variables, and Calculations If you compile and execute this example, it produces the following output: “We’ll make our escapes in sequence”, he said. The program’s over, it’s time take make a beep beep.
How It Works The first line in main() defines the variable newline and initializes it with a character defined by the escape sequence for a new line. You can then use newline instead of endl from the standard library. After writing newline to cout, you output a string that uses the escape sequences for a double quote (\”) and a single quote (\’). You don’t have to use the escape sequence for a single quote here because the string is delimited by double quotes and the compile would recognize a single quote character as just that, and not a delimiter. You must use the escape sequences for the double quotes in the string, though. The string starts with a newline escape sequence followed by a tab escape sequence, so the output line is indented by the tab distance. The string also ends with two instances of the escape sequence for a beep, so you should hear a double beep from your PC’s speaker.
Calculating in C++ This is where you actually start doing something with the data that you enter. You know how to carry out simple input and output; now you are beginning the bit in the middle, the “processing” part of a C++ program. Almost all of the computational aspects of C++ are fairly intuitive, so you should slice through this like a hot knife through butter.
The Assignment Statement You have already seen examples of the assignment statement. A typical assignment statement looks like this: whole = part1 + part2 + part3;
The assignment statement enables you to calculate the value of an expression which appears on the right hand side of the equals sign, in this case the sum of part1, part2, and part3, and store the result in the variable specified on the left hand side, in this case the variable with the name whole. In this statement, the whole is exactly the sum of its parts and no more. Note how the statement, as always, ends with a semicolon. You can also write repeated assignments such as, a = b = 2;
This is equivalent to assigning the value 2 to b and then assigning the value of b to a, so both variables will end up storing the value 2.
67
Chapter 2 Understanding Lvalues and Rvalues An lvalue is something that refers to an address in memory and is so called because any expression that results in an lvalue can appear on the left of the equals sign in an assignment statement. Most variables are lvalues, because they specify a place in memory. However, as you’ll see, there are variables that aren’t lvalues and can’t appear on the left of an assignment because their values have been defined as constant. The variables a and b that appear in the preceding paragraph are lvalues, whereas the result of evaluating the expression a+b would not be, because its result doesn’t determine an address in memory where a value might be stored. The result of an expression that is not an lvalue is referred to as an rvalue. Lvalues will pop up at various times throughout the book, sometimes where you least expect them, so keep the idea in mind.
Arithmetic Operations The basic arithmetic operators you have at your disposal are addition, subtraction, multiplication, and division, represented by the symbols +, -, * and /, respectively. These operate generally as you would expect, with the exception of division which has a slight aberration when working with integer variables or constants, as you’ll see. You can write statements such as the following: netPay = hours * rate - deductions;
Here, the product of hours and rate will be calculated and then deductions subtracted from the value produced. The multiply and divide operators are executed before addition and subtraction, as you would expect. I will discuss the order of execution of the various operators in expressions more fully later in this chapter. The overall result of evaluating the expression hours * rate - deductions will be stored in the variable netPay. The minus sign used in the last statement has two operands — it subtracts the value of its right operand from the value of its left operand. This is called a binary operation because two values are involved. The minus sign can also be used with one operand to change the sign of the value to which it is applied, in which case it is called a unary minus. You could write this: int a = 0; int b = -5; a = -b;
// Changes the sign of the operand
Here, a will be assigned the value +5 because the unary minus changes the sign of the value of the operand b. Note that an assignment is not the equivalent of the equations you saw in high school algebra. It specifies an action to be carried out rather than a statement of fact. The expression to the right of the assignment operator is evaluated and the result is stored in the lvalue — typically a variable — that appears on the left. Look at this statement: number = number + 1;
68
Data, Variables, and Calculations This means “add 1 to the current value stored in number and then store the result back in number.” As a normal algebraic statement it wouldn’t make sense.
Try It Out
Exercising Basic Arithmetic
You can exercise basic arithmetic in C++ by calculating how many standard rolls of wallpaper are needed to paper a room. The following example does this: // Ex2_05.cpp // Calculating how many rolls of wallpaper are required for a room #include using std::cout; using std::cin; using std::endl; int main() { double height = 0.0, width = 0.0, length = 0.0; // Room dimensions double perimeter = 0.0; // Room perimeter const double rollwidth = 21.0; const double rolllength = 12.0*33.0;
// Standard roll width // Standard roll length(33ft.)
int strips_per_roll = 0; int strips_reqd = 0; int nrolls = 0;
// Number of strips in a roll // Number of strips needed // Total number of rolls
cout << endl // Start a new line << “Enter the height of the room in inches: “; cin >> height; cout
<< endl // Start a new line << “Now enter the length and width in inches: “; cin >> length >> width; strips_per_roll = rolllength / height; perimeter = 2.0*(length + width); strips_reqd = perimeter / rollwidth; nrolls = strips_reqd / strips_per_roll;
// // // //
Get number of strips per roll Calculate room perimeter Get total strips required Calculate number of rolls
cout << endl << “For your room you need “ << nrolls << “ rolls of wallpaper.” << endl; return 0; }
Unless you are more adept than I am at typing, chances are there will be a few errors when you compile this for the first time. Once you have fixed the typos, it will compile and run just fine. You’ll get a couple of warning messages from the compiler. Don’t worry about them — the compiler is just making sure you understand what’s going on. I’ll explain the reason for the error messages in a moment.
69
Chapter 2 How It Works One thing needs to be clear at the outset — I assume no responsibility for your running out of wallpaper as a result of using this program! As you’ll see, all errors in the estimate of the number of rolls required are due to the way C++ works and to the wastage that inevitably occurs when you hang your own wallpaper — usually 50 percent +! I’ll work through the statements in this example in sequence, picking out the interesting, novel, or even exciting features. The statements down to the start of the body of main() are familiar territory by now, so I will take those for granted. A couple of general points about the layout of the program are worth noting. First, the statements in the body of main() are indented to make the extent of the body easier to see, and second, various groups of statements are separated by a blank line to indicate that they are functional groups. Indenting statements is a fundamental technique in laying out program code in C++. You will see that this is applied universally to provide visual cues to help you identify the various logical blocks in a program.
The const Modifier You have a block of declarations for the variables used in the program right at the beginning of the body of main(). These statements are also fairly familiar, but there are two that contain some new features: const double rollwidth = 21.0; const double rolllength = 12.0*33.0;
// Standard roll width // Standard roll length(33ft.)
They both start out with a new keyword: const. This is a type modifier that indicates that the variables are not just of type double but are also constants. Because you effectively tell the compiler that these are constants, the compiler will check for any statements that attempt to change the values of these variables, and if it finds any, it will generate an error message. A variable declared as const is not an lvalue and, therefore, can’t legally be placed on the left of an assignment operation. You could check this out by adding, anywhere after the declaration of rollwidth, a statement such as: rollwidth = 0;
You will find the program no longer compiles, returning ‘error C2166: l-value specifies const object’.
It can be very useful to define constants that you use in a program by means of const variable types, particularly when you use the same constant several times in a program. For one thing, it is much better than sprinkling literals throughout your program that may not have blindingly obvious meanings; with the value 42 in a program you could be referring to the meaning of life, the universe, and everything, but if you use a const variable with the name myAge that has a value of 42, it becomes obvious that you are not. For another thing, if you need to change the value of a const variable that you are using, you will need to change its definition only in a source file to ensure that the change automatically appears throughout. You’ll see this technique used quite often.
Constant Expressions The const variable rolllength is also initialized with an arithmetic expression (12.0*33.0). Being able to use constant expressions to initialize variables saves having to work out the value yourself. It can
70
Data, Variables, and Calculations also be more meaningful, as it is in this case because 33 feet times 12 inches is a much clearer expression of what the value represents than simply writing 396. The compiler will generally evaluate constant expressions accurately, whereas if you do it yourself, depending on the complexity of the expression and your ability to number-crunch, there is a finite probability that it may be wrong. You can use any expression that can be calculated as a constant at compile time, including const objects that you have already defined. So, for instance, if it was useful in the program to do so, you could declare the area of a standard roll of wallpaper as: const double rollarea = rollwidth*rolllength;
This statement would need to be placed after the declarations for the two const variables used in the initialization of rollarea because all the variables that appear in a constant expression must be known to the compiler at the point in the source file where the constant expression appears.
Program Input After declaring some integer variables, the next four statements in the program handle input from the keyboard: cout << endl // Start a new line << “Enter the height of the room in inches: “; cin >> height; cout << endl // Start a new line << “Now enter the length and width in inches: “; cin >> length >> width;
Here you have written text to cout to prompt for the input required and then read the input from the keyboard using cin, which is the standard input stream. You first obtain the value for the room height and then read the length and width successively. In a practical program, you would need to check for errors and possibly make sure that the values that are read are sensible, but you don’t have enough knowledge to do that yet!
Calculating the Result You have four statements involved in calculating the number of standard rolls of wallpaper required for the size of room given: strips_per_roll = rolllength / height; perimeter = 2.0*(length + width); strips_reqd = perimeter / rollwidth; nrolls = strips_reqd / strips_per_roll;
// // // //
Get number of strips in a roll Calculate room perimeter Get total strips required Calculate number of rolls
The first statement calculates the number of strips of paper with a length corresponding to the height of the room that you can get from a standard roll, by dividing one into the other. So, if the room is 8 feet high, you divide 96 into 396, which would produce the floating-point result 4.125. There is a subtlety here, however. The variable where you store the result, strips_per_roll, was declared as int, so it can store only integer values. Consequently, any floating-point value to be stored as an integer is rounded down to the nearest integer, 4 in this case, and this value is stored. This is actually the result that you want here because, although they may fit under a window or over a door, fractions of a strip are best ignored when estimating.
71
Chapter 2 The conversion of a value from one type to another is called casting. This particular example is called an implicit cast, because the code doesn’t explicitly state that a cast is needed, and the compiler has to work it out for itself. The two warnings you got during compilation were issued because the implicit casts that were inserted implied information could be lost due to the conversion from one type to another. You should beware when using implicit casts. Compilers do not always supply a warning that an implicit cast is being made, and if you are assigning a value of one type to a variable of a type with a lesser range of values, then there is always a danger that you will lose information. If there are implicit casts in your program that you have included accidentally, they may represent bugs that may be difficult to locate. Where such an assignment is unavoidable, you can specify the conversion explicitly to demonstrate that it is no accident and that you really meant to do it. You do this by making an explicit cast of the value on the right of the assignment to int, so the statement would become: strips_per_roll = static_cast(rolllength / height);
// Get number of strips // in a roll
The addition of static_cast with the parentheses around the expression on the right tells the compiler explicitly that you want to convert the value of the expression to int. Although this means that you still lose the fractional part of the value, the compiler assumes that you know what you are doing and will not issue a warning. You’ll see more about static_cast<>() and other types of explicit casting, later in this chapter. Note how you calculate the perimeter of the room in the next statement. To multiply the sum of the length and the width by two, you enclose the expression summing the two variables between paren-
theses. This ensures that the addition is performed first and the result is multiplied by 2.0 to produce the correct value for the perimeter. You can use parentheses to make sure that a calculation is carried out in the order you require because expressions in parentheses are always evaluated first. Where there are nested parentheses, the expressions within the parentheses are evaluated in sequence, from the innermost to the outermost. The third statement, calculating how many strips of paper are required to cover the room, uses the same effect that you observed in the first statement: The result is rounded down to the nearest integer because it is to be stored in the integer variable, strips_reqd. This is not what you need in practice. It would be best to round up for estimating, but you don’t have enough knowledge of C++ to do this yet. Once you have read the next chapter, you can come back and fix it! The last arithmetic statement calculates the number of rolls required by dividing the number of strips required (an integer) by the number of strips in a roll (also an integer). Because you are dividing one integer by another, the result has to be an integer, and any remainder is ignored. This would still be the case if the variable nrolls were floating point. The integer value resulting from the expression would be converted to floating-point form before it was stored in nrolls. The result that you obtain is essentially the same as if you had produced a floating-point result and rounded down to the nearest integer. Again, this is not what you want, so if you want to use this, you will need to fix it.
72
Data, Variables, and Calculations Displaying the Result The result of the calculation is displayed by the following statement: cout << endl << “For your room you need “ << nrolls << “ rolls of wallpaper.” << endl;
This is a single output statement spread over three lines. It first outputs a newline character and then the text string “For your room you need “. This is followed by the value of the variable nrolls and finally the text string “ rolls of wallpaper.”. As you can see, output statements are very easy in C++. Finally, the program ends when this statement is executed: return 0;
The value zero here is a return value that, in this case, will be returned to the operating system. You will see more about return values in Chapter 5.
Calculating a Remainder You saw in the last example that dividing one integer value by another produces an integer result that ignores any remainder, so that 11 divided by 4 gives the result 2. Because the remainder after division can be of great interest, particularly when you are dividing cookies amongst children, for example, C++ provides a special operator, %, for this. So you can write the following statements to handle the cookiesharing problem: int residue = 0, cookies = 19, children = 5; residue = cookies % children;
The variable residue will end up with the value 4, the number left after dividing 19 by 5. To calculate how many cookies each child receives, you just need to use division, as in the statement: each = cookies / children;
Modifying a Variable It’s often necessary to modify the existing value of a variable, such as by incrementing it or doubling it. You could increment a variable called count using the statement: count = count + 5;
This simply adds 5 to the current value stored in count and stores the result back in count, so if count started out at 10, it would end up as 15. You also have an alternative, shorthand way of writing the same thing in C++: count += 5;
73
Chapter 2 This says, “Take the value in count, add 5 to it, and store the result back in count.” We can also use other operators with this notation. For example, count *= 5;
has the effect of multiplying the current value of count by 5 and storing the result back in count. In general, you can write statements of the form, lhs op=
rhs;
where op is any of the following operators: +
-
*
/
%
<<
>>
&
^
>
You have already met the first five of these operators, and you’ll see the others, which are the shift and logical operators, later in this chapter. lhs stands for any legal expression for the left-hand side of the statement and is usually (but not necessarily) a variable name. rhs stands for any legal expression on the right-hand side of the statement. The general form of the statement is equivalent to this: lhs = lhs op (rhs);
The parentheses around rhs imply that this expression is evaluated first, and the result becomes the right operand for op. This means that you can write statements such as a /= b + c;
This will be identical in effect to this statement: a = a/(b + c);
Thus, the value of a will be divided by the sum of b and c, and the result will be stored back in a.
The Increment and Decrement Operators I will now introduce some unusual arithmetic operators called the increment and decrement operators, as you will find them to be quite an asset once you get further into applying C++ in earnest. These are unary operators that you use to increment or decrement the value stored in a variable that holds an integral value. For example, assuming the variable count is of type int, the following three statements all have exactly the same effect: count = count + 1;
74
count += 1;
++count;
Data, Variables, and Calculations They each increment the variable count by 1. The last form, using the increment operator, is clearly the most concise. The increment operator not only changes the value of the variable to which you apply it but also results in a value. Thus, using the increment operator to increase the value of a variable by 1 can also appear as part of a more complex expression. If incrementing a variable using the ++ operator, as in ++count, is contained within another expression, then the action of the operator is to first increment the value of the variable and then use the incremented value in the expression. For example, suppose count has the value 5, and you have defined a variable total of type int. Suppose you write the following statement: total = ++count + 6;
This results in count being incremented to 6, and this result is added to 6, so total is assigned the value 12. So far, you have written the increment operator, ++, in front of the variable to which it applies. This is called the prefix form of the increment operator. The increment operator also has a postfix form, where the operator is written after the variable to which it applies; the effect of this is slightly different. The variable to which the operator applies is incremented only after its value has been used in context. For example, reset count to the value 5 and rewrite the previous statement as: total = count++ + 6;
Then total is assigned the value 11, because the initial value of count is used to evaluate the expression before the increment by 1 is applied. The preceding statement is equivalent to the two statements: total = count + 6; ++count;
The clustering of ‘+’ signs, in the example of the preceding postfix form, is likely to lead to confusion. Generally, it isn’t a good idea to write the increment operator in the way that I have written it here. It would be clearer to write: total = 6 + count++;
Where you have an expression such as a++ + b, or even a+++b, it becomes less obvious what is meant or what the compiler will do. They are actually the same, but in the second case you might really have meant a + ++b, which is different. It evaluates to one more than the other two expressions. Exactly the same rules that I have discussed in relation to the increment operator apply to the decrement operator, --. For example, if count has the initial value 5, then the statement total = --count + 6;
results in total having the value 10 assigned, whereas total = 6 + count--;
75
Chapter 2 sets the value of total to 11. Both operators are usually applied to integers, particularly in the context of loops, as we shall see in Chapter 3. You will see in later chapters that they can also be applied to other data types in C++, notably variables that store addresses.
Try It Out
The Comma Operator
The comma operator allows you to specify several expressions where normally only one might occur. This is best understood by looking at an example that demonstrates how it works: // Ex2_06.cpp // Exercising the comma operator #include using std::cout; using std::endl; int main() { long num1 = 0, num2 = 0, num3 = 0, num4 = 0; num4 = (num1 = 10, num2 = 20, num3 = 30); cout << endl << “The value of a series of expressions “ << “is the value of the rightmost: “ << num4; cout << endl; return 0; }
How It Works If you compile and run this program, you will get this output: The value of a series of expressions is the value of the rightmost: 30
This is fairly self-explanatory. The variable num4 receives the value of the last of the series of three assignments, the value of an assignment being the value assigned to the left-hand side. The parentheses in the assignment for num4 are essential. You could try executing this without them to see the effect. Without the parentheses, the first expression separated by commas in the series will become: num4 = num1 = 10
So, num4 will have the value 10. Of course, the expressions separated by the comma operator don’t have to be assignments. You could equally well write the following statements: long num1 = 1, num2 = 10, num3 = 100, num4 = 0; num4 = (++num1, ++num2, ++num3);
76
Data, Variables, and Calculations The effect of the assignment statement will be to increment the variables num1, num2, and num3 by 1, and to set num4 to the value of the last expression which will be 101. This example is aimed at illustrating the effect of the comma operator and is not an example of how to write good code.
The Sequence of Calculation So far, I haven’t talked about how you arrive at the sequence of calculations involved in evaluating an expression. It generally corresponds to what you will have learned at school when dealing with basic arithmetic operators, but there are many other operators in C++. To understand what happens with these you need to look at the mechanism used in C++ to determine this sequence. It’s referred to as operator precedence.
Operator Precedence Operator precedence orders the operators in a priority sequence. In any expression, operators with the highest precedence are always executed first, followed by operators with the next highest precedence, and so on, down to those with the lowest precedence of all. The precedence of the operators in C++ is shown in the following table: Operators
Associativity
::
Left
()
[]
->
Left
.
! ~ +(unary) -(unary) ++ -- &(unary) *(unary) (typecast) static_cast const_cast dynamic_cast reinterpret_cast sizeof new delete...typeid
Right
.*(unary)
Left
*
/
+
-
<< <
->*
Left
%
Left Left
>> <=
==
>
Left
>=
Left
!=
&
Left
^
Left
>
Left
&&
Left
>>
Left
?:(conditional operator)
Right
= ,
*=
/=
%=
+=
-=
&=
^=
>=
<<=
>>=
Right Left
77
Chapter 2 There are a lot of operators here that you haven’t seen yet, but you will know them all by the end of the book. Rather than spreading them around, I have put all the C++ operators in the precedence table so that you can always refer to it if you are uncertain about the precedence of one operator relative to another. Operators with the highest precedence appear at the top of the table. All the operators that appear in the same cell in the table have equal precedence. If there are no parentheses in an expression, operators with equal precedence are executed in a sequence determined by their associativity. Thus, if the associativity is “left,” the left-most operator in an expression is executed first, progressing through the expression to the right-most. This means that an expression such as a + b + c + d is executed as though it was written (((a + b) + c) + d) because binary + is left-associative. Note that where an operator has a unary (working with one operand) and a binary (working with two operands) form, the unary form is always of a higher precedence and is, therefore, executed first.
You can always override the precedence of operators by using parentheses. Because there are so many operators in C++, it’s sometimes hard to be sure what takes precedence over what. It is a good idea to insert parentheses to make sure. A further plus is that parentheses often make the code much easier to read.
Variable Types and Casting Calculations in C++ can be carried out only between values of the same type. When you write an expression involving variables or constants of different types, for each operation to be performed the compiler has to convert the type of one of the operands to match that of the other. This conversion process is called casting. For example, if you want to add a double value to a value of an integer type, the integer value is first converted to double, after which the addition is carried out. Of course, the variable that contains the value to be cast is itself not changed. The compiler will store the converted value in a temporary memory location that will be discarded when the calculation is finished. There are rules that govern the selection of the operand to be converted in any operation. Any expression to be calculated breaks down into a series of operations between two operands. For example, the expression 2*3-4+5 amounts to the series 2*3 resulting in 6, 6-4 resulting in 2, and finally 2+5 resulting in 7. Thus, the rules for casting operands where necessary need to be defined only in terms of decisions about pairs of operands. So, for any pair of operands of different types, the following rules are checked in the order that they are written. When one applies, that rule is used.
Rules for Casting Operands 1. 2. 3. 4.
78
If either operand is of type long double, the other is converted to long double. If either operand is of type double, the other is converted to double. If either operand is of type float, the other is converted to float. Any operand of type char, signed char, unsigned char, short, or unsigned short is converted to type int.
Data, Variables, and Calculations 5.
An enumeration type is converted to the first of int, unsigned int, long, or unsigned long that accommodates the range of the enumerators.
6. 7.
If either operand is of type unsigned long, the other is converted to unsigned long.
8.
If either operand is of type long, the other is converted to type long.
If one operand is of type long and the other is of type unsigned int, then both operands are converted to type unsigned long.
This looks and reads as though it is incredibly complicated, but the basic principle is to always convert the value that has the type that is of a more limited range to the type of the other value. This maximizes the likelihood of being able to accommodate the result. You could try these rules on a hypothetical expression to see how they work. Suppose that you have a sequence of variable declarations as follows: double value = 31.0; int count = 16; float many = 2.0f; char num = 4;
Also suppose that you have the following rather arbitrary arithmetic statement: value = (value - count)*(count - num)/many + num/many;
You can now work out what casts the compiler will apply in the execution of the statement. The first operation is to calculate (value - count). Rule 1 doesn’t apply but Rule 2 does, so the value of count is converted to double and the double result 15.0 is calculated. Next (count - num) must be evaluated, and here the first rule in sequence that applies is Rule 4, so num is converted from char to int and the result 12 is produced as a value of type int. The next calculation is the product of the first two results, a double 15.0 and an int 12. Rule 2 applies here, and the 12 is converted to 12.0 as double, and the double result 180.0 is produced. This result now has to be divided by many, so Rule 2 applies again, and the value of many is converted to double before generating the double result 90.0. The expression num/many is calculated next, and here Rule 3 applies to produce the float value 2.0f after converting the type of num from char to float. Lastly, the double value 90.0 is added to the float value 2.0f for which Rule 2 applies, so after converting the 2.0f to 2.0 as double, the final result of 92.0 is stored in value. In spite of the preceding sequence reading a bit like The Auctioneer’s Song, you should get the general idea.
Casts in Assignment Statements As you saw in example Ex2_05.cpp earlier in this chapter, you can cause an implicit cast by writing an expression on the right-hand side of an assignment that is of a different type to the variable on the
79
Chapter 2 left-hand side. This can cause values to be changed and information to be lost. For instance, if you assign a float or double value to a variable of type int or a long, the fractional part of the float or double will be lost and just the integer part will be stored. (You may lose even more information if your floatingpoint variable exceeds the range of values available for the integer type concerned.) For example, after executing the following code fragment, int number = 0; float decimal = 2.5f; number = decimal;
the value of number will be 2. Note the f at the end of the constant 2.5f. This indicates to the compiler that this constant is single precision floating point. Without the f, the default would have been type double. Any constant containing a decimal point is floating point. If you don’t want it to be double precision, you need to append the f. A capital letter F would do the job just as well.
Explicit Casts With mixed expressions involving the basic types, your compiler automatically arranges casting where necessary, but you can also force a conversion from one type to another by using an explicit cast. To cast the value of an expression to a given type, you write the cast in the form: static_cast(expression)
The keyword static_cast reflects the fact that the cast is checked statically — that is, when your program is compiled. No further checks are made when you execute the program to see if this cast is safe to apply. Later, when you get to deal with classes, you will meet dynamic_cast, where the conversion is checked dynamically — that is, when the program is executing. There are also two other kinds of cast, const_cast for removing the const-ness of an expression and reinterpret_cast, which is an unconditional cast, but I’ll say no more about these here. The effect of the static_cast operation is to convert the value that results from evaluating expression to the type that you specify between the angled brackets. The expression can be anything from a single variable to a complex expression involving lots of nested parentheses. Here’s a specific example of the use of static_cast<>(): double value1 = 10.5; double value2 = 15.5; int whole_number = static_cast(value1) + static_cast(value2);
The initializing value for the variable whole_number is the sum of the integral parts of value1 and value2, so they are each explicitly cast to type int. The variable whole_number will therefore have the initial value 25. The casts do not affect the values stored in value1 and value2, which will remain as 10.5 and 15.5, respectively. The values 10 and 15 produced by the casts are just stored temporarily for use in the calculation and then discarded. Although both casts cause a loss of information in the calculation, the compiler will always assume that you know what you are doing when you specify a cast explicitly.
80
Data, Variables, and Calculations Also, as I described in Ex2_05.cpp relating to assignments involving different types, you can always make it clear that you know the cast is necessary by making it explicit: strips_per_roll = static_cast(rolllength / height);
//Get number of strips // in a roll
You can write an explicit cast for a numerical value to any numeric type, but you should be conscious of the possibility of losing information. If you cast a value of type float or double to type long, for example, you will lose the fractional part of the value when it is converted, so if the value started out as less than 1.0, the result will be 0. If you cast a value of type double to type float, you will lose accuracy because a float variable has only 7 digits precision, whereas double variables maintain 15. Even casting between integer types provides the potential for losing data, depending on the values involved. For example, the value of an integer of type long can exceed the maximum that you can store in a variable of type short, so casting from a long value to a short may lose information. In general, you should avoid casting as far as possible. If you find that you need a lot of casts in your program, the overall design of your program may well be at fault. You need to look at the structure of the program and the ways in which you have chosen data types to see whether you can eliminate, or at least reduce, the number of casts in your program.
Old-Style Casts Prior to the introduction of static_cast<>() (and the other casts: const_cast<>(), dynamic_cast<>(), and reinterpret_cast<>(), which we’ll discuss later in the book) into C++, an explicit cast of the result of an expression to another type was written as: (the_type_to_convert_to)expression
The result of expression is cast to the type between the parentheses. For example, the statement to calculate strips_per_roll in the previous example could be written: strips_per_roll = (int)(rolllength / height);
//Get number of strips in a roll
Essentially, there are four different kinds of casts, and the old-style casting syntax covers them all. Because of this, code using the old-style casts is more error prone — it is not always clear what you intended, and you may not get the result you expected. Although you will still see the old style of casting used extensively (it’s still part of the language and you will see it in MFC code for historical reasons), I strongly recommend that you stick to using only the new casts in your code.
The Bitwise Operators The bitwise operators treat their operands as a series of individual bits rather than a numerical value. They work only with integer variables or integer constants as operands, so only data types short, int, long, signed char, and char, as well as the unsigned variants of these, can be used. The bitwise operators are useful in programming hardware devices, where the status of a device is often represented as a series of individual flags (that is, each bit of a byte may signify the status of a different aspect of the device), or for any situation where you might want to pack a set of on-off flags into a single variable. You will see them in action when you look at input/output in detail, where single bits are used to control various options in the way data is handled.
81
Chapter 2 There are six bitwise operators: & bitwise AND
> bitwise OR
^ bitwise exclusive OR
~ bitwise NOT
>> shift right
<< shift left
The next sections take a look at how each of them works.
The Bitwise AND The bitwise AND, &, is a binary operator that combines corresponding bits in its operands in a particular way. If both corresponding bits are 1, the result is a 1 bit, and if either or both bits are 0, the result is a 0 bit. The effect of a particular binary operator is often shown using what is called a truth table. This shows, for various possible combinations of operands, what the result is. The truth table for & is as follows: Bitwise AND
0
1
0
0
0
1
0
1
For each row and column combination, the result of & combining the two is the entry at the intersection of the row and column. You can see how this works in an example: char letter1 = ‘A’, letter2 = ‘Z’, result = 0; result = letter1 & letter2;
You need to look at the bit patterns to see what happens. The letters ‘A’ and ‘Z’ correspond to hexadecimal values 0x41 and 0x5A, respectively (see Appendix B for ASCII codes). The way in which the bitwise AND operates on these two values is shown in Figure 2-8.
letter1: 0×41
letter2: 0×5A
result: 0×40
Figure 2-8
82
0
1
0
0
0
0
0
1
& & & &
& & & &
0
1
0
1
1
0
1
0
=
=
=
=
=
=
=
=
0
1
0
0
0
0
0
0
Data, Variables, and Calculations You can confirm this by looking at how corresponding bits combine with & in the truth table. After the assignment, result will have the value 0x40, which corresponds to the character ‘@’. Because the & produces zero if either bit is zero, you can use this operator to make sure that unwanted bits are set to 0 in a variable. You achieve this by creating what is called a “mask” and combining with the original variable using &. You create the mask by specifying a value that has 1 where you want to keep a bit, and 0 where you want to set a bit to zero. The result of ANDing the mask with another integer will be 0 bits where the mask bit is 0, and the same value as the original bit in the variable where the mask bit is 1. Suppose you have a variable letter of type char where, for the purposes of illustration, you want to eliminate the high order 4 bits, but keep the low order 4 bits. This is easily done by setting up a mask as 0x0F and combining it with the value of letter using & like this: letter = letter & 0x0F;
or, more concisely: letter &= 0x0F;
If letter started out as 0x41, it would end up as 0x01 as a result of either of these statements. This operation is shown in Figure 2-9.
letter: 0×41
mask: 0×0F
result: 0×01
0
1
0
0
0
0
0
1
& & & &
& & & &
0
0
0
0
1
1
1
1
=
=
=
=
=
=
=
=
0
0
0
0
0
0
0
1
Figure 2-9
The 0 bits in the mask cause corresponding bits in letter to be set to 0, and the 1 bits in the mask cause corresponding bits in letter to be kept as they are. Similarly, you can use a mask of 0xF0 to keep the 4 high order bits, and zero the 4 low order bits. Therefore, this statement, letter &= 0xF0;
will result in the value of letter being changed from 0x41 to 0x40.
83
Chapter 2 The Bitwise OR The bitwise OR, >, sometimes called the inclusive OR, combines corresponding bits such that the result is a 1 if either operand bit is a 1, and 0 if both operand bits are 0. The truth table for the bitwise OR is: Bitwise OR
0
1
0
0
1
1
1
1
You can exercise this with an example of how you could set individual flags packed into a variable of type int. Suppose that you have a variable called style of type short that contains 16 individual 1-bit flags. Suppose further that you are interested in setting individual flags in the variable style. One way of doing this is by defining values that you can combine with the OR operator to set particular bits on. To use in setting the rightmost bit, you can define: short vredraw = 0x01;
For use in setting the second-to-rightmost bit, you could define the variable hredraw as: short hredraw = 0x02;
So you could set the rightmost two bits in the variable style to 1 with the statement: style = hredraw > vredraw;
The effect of this statement is illustrated in Figure 2-10.
hredraw: 0×02
0
0
0
0
OR OR OR OR
vredraw: 0×01
style: 0×03
Figure 2-10
84
0
0
0
0
OR OR OR OR
0
0
0
0
OR OR OR OR
0
0
1
0
OR OR OR OR
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
1
Data, Variables, and Calculations Because the OR operation results in 1 if either of two bits is a 1, ORing the two variables together produces a result with both bits set on. A very common requirement is to be able to set flags in a variable without altering any of the others which may have been set elsewhere. You can do this quite easily with a statement such as: style >= hredraw > vredraw;
This statement will set the two rightmost bits of the variable style to 1, leaving the others at whatever they were before the execution of this statement.
The Bitwise Exclusive OR The exclusive OR, ^, is so called because it operates similarly to the inclusive OR but produces 0 when both operand bits are 1. Therefore, its truth table is as follows: Bitwise EOR
0
1
0
0
1
1
1
0
Using the same variable values that we used with the AND, you can look at the result of the following statement: result = letter1 ^ letter2;
This operation can be represented as: letter1 0100 0001 letter2 0101 1010
EORed together produce: result 0001 1011
The variable result is set to 0x1B, or 27 in decimal notation. The ^ operator has a rather surprising property. Suppose that you have two char variables, first with the value ‘A’, and last with the value ‘Z’, corresponding to binary values 0100 0001 and 0101 1010. If you write the statements first ^= last; last ^= first; first ^= last;
// Result first is 0001 1011 // Result last is 0100 0001 // Result first is 0101 1010
the result of these is that first and last have exchanged values without using any intermediate memory location. This works with any integer values.
85
Chapter 2 The Bitwise NOT The bitwise NOT, ~, takes a single operand for which it inverts the bits: 1 becomes 0, and 0 becomes 1. Thus, if you execute the statement result = ~letter1;
if letter1 is 0100 0001, the variable result will have the value 1011 1110, which is 0xBE, or 190 as a decimal value.
The Bitwise Shift Operators These operators shift the value of an integer variable a specified number of bits to the left or right. The operator >> is for shifts to the right, while << is the operator for shifts to the left. Bits that “fall off” either end of the variable are lost. Figure 2-11 shows the effect of shifting the 2-byte variable left and right, with the initial value shown.
Decimal 16,387 in binary is:
0100000000000011
0100000000000011 Shift left 2:
Zeros are shifted in from the right
––––––––––
These two bits are shifted out and lost
0000000000001100 =12
Zeros are shifted in from the left
0100000000000011
Shift right 2:
––––––––––
0001000000000000 = 4096
These two bits are shifted out and lost
Figure 2-11
You declare and initialize a variable called number with the statement: unsigned int number = 16387U;
As you saw earlier in this chapter, you write unsigned integer literals with a letter U or u appended to the number. You can shift the contents of this variable to the left with the statement: number <<= 2;
86
// Shift left two bit positions
Data, Variables, and Calculations The left operand of the shift operator is the value to be shifted, and the number of bit positions that the value is to be shifted is specified by the right operand. The illustration shows the effect of the operation. As you can see, shifting the value 16,387 two positions to the left produces the value 12. The rather drastic change in the value is the result of losing the high order bit when it is shifted out. You can also shift the value to the right. Let’s reset the value of number to its initial value of 16,387. Then you can write: number >>= 2;
// Shift right two bit positions
This shifts the value 16,387 two positions to the right, storing the value 4,096. Shifting right two bits is effectively dividing the value by 4 (without remainder). This is also shown in the illustration. As long as bits are not lost, shifting n bits to the left is equivalent to multiplying the value by 2, n times. In other words, it is equivalent to multiplying by 2n. Similarly, shifting right n bits is equivalent to dividing by 2n. But beware: as you saw with the left shift of the variable number, if significant bits are lost, the result is nothing like what you would expect. However, this is no different from the multiply operation. If you multiplied the 2-byte number by 4 you would get the same result, so shifting left and multiply are still equivalent. The problem of accuracy arises because the value of the result of the multiplication is outside the range of a 2-byte integer. You might imagine that confusion could arise between the operators that you have been using for input and output and the shift operators. As far as the compiler is concerned, the meaning will always be clear from the context. If it isn’t, the compiler will generate a message, but you need to be careful. For example, if you want to output the result of shifting a variable number left by 2 bits, you could write the following statement: cout << (number << 2);
Here, the parentheses are essential. Without them, the shift operator will be interpreted by the compiler as a stream operator, so you won’t get the result that you intended; the output will be the value of number followed by the value 2. In the main, the right shift operation is similar to the left shift. For example, suppose the variable number has the value 24, and you execute the following statement: number >>= 2;
This will result in number having the value 6, effectively dividing the original value by 4. However, the right shift operates in a special way with signed integer types that are negative (that is, the sign bit, which is the leftmost bit, is 1). In this case, the sign bit is propagated to the right. For example, declare and initialize a variable number of type char with the value -104 in decimal: char number = -104;
// Binary representation is 1001 1000
Now you can shift it right 2 bits with the operation: number >>= 2;
// Result 1110 0110
The decimal value of the result is -26, as the sign bit is repeated. With operations on unsigned integer types, of course, the sign bit is not repeated and zeros appear.
87
Chapter 2
Understanding Storage Duration and Scope All variables have a finite lifetime when your program executes. They come into existence from the point at which you declare them and then, at some point, they disappear — at the latest, when your program terminates. How long a particular variable lasts is determined by a property called its storage duration. There are three different kinds of storage duration that a variable can have: ❑
Automatic storage duration
❑
Static storage duration
❑
Dynamic storage duration
Which of these a variable will have depends on how you create it. I will defer discussion of variables with dynamic storage duration until Chapter 4, but you will be exploring the characteristics of the other two in this chapter. Another property that variables have is scope. The scope of a variable is simply that part of your program over which the variable name is valid. Within a variable’s scope, you can legally refer to it, to set its value or use it in an expression. Outside of the scope of a variable, you cannot refer to its name — any attempt to do so will cause a compiler error. Note that a variable may still exist outside of its scope, even though you cannot refer to it by name. You will see examples of this situation a little later in this discussion. All of the variables that you have declared up to now have had automatic storage duration, and are therefore called automatic variables. Let’s take a closer look at these first.
Automatic Variables The variables that you have declared so far have been declared within a block — that is, within the extent of a pair of braces. These are called automatic variables and are said to have local scope or block scope. An automatic variable is “in scope” from the point at which it is declared until the end of the block containing its declaration. The space that an automatic variable occupies is allocated automatically in a memory area called the stack that is set aside specifically for this purpose. The default size for the stack is 1MB, which is adequate for most purposes, but if it should turn out to be insufficient, you can increase the size of the stack by setting the /STACK option for the project to a value of your choosing. An automatic variable is “born” when it is defined and space for it is allocated on the stack, and it automatically ceases to exist at the end of the block containing the definition of the variable. This will be at the closing brace matching the first opening brace that precedes the declaration of the variable. Every time the block of statements containing a declaration for an automatic variable is executed, the variable is created anew, and if you specified an initial value for the automatic variable, it will be reinitialized each time it is created. When an automatic variable dies, its memory on the stack will be freed for use by other automatic variables There is a keyword, auto, which you can use to specify automatic variables, but it is rarely used since it is implied by default. What follows is an example of what I’ve discussed so far about scope.
88
Data, Variables, and Calculations Try It Out
Automatic Variables
I can demonstrate the effect of scope on automatic variables with the following example: // Ex2_07.cpp // Demonstrating variable scope #include using std::cout; using std::endl; int main() { // Function scope starts here int count1 = 10; int count3 = 50; cout << endl << “Value of outer count1 = “ << count1 << endl; {
}
// int count1 = 20; // int count2 = 30; cout << “Value of inner count1 = << endl; count1 += 3; // count3 += count2; //
cout << << << <<
New scope starts here... This hides the outer count1 “ << count1 This affects the inner count1 ...and ends here
“Value of outer count1 = “ << count1 endl “Value of outer count3 = “ << count3 endl;
// cout << count2 << endl;
// uncomment to get an error
return 0; }
// Function scope ends here
The output from this example will be: Value Value Value Value
of of of of
outer inner outer outer
count1 count1 count1 count3
= = = =
10 20 10 80
How It Works The first two statements declare and define two integer variables, count1 and count3, with initial values of 10 and 50, respectively. Both these variables exist from this point to the closing brace at the end of the program. The scope of these variables also extends to the closing brace at the end of main().
89
Chapter 2 Remember that the lifetime and scope of a variable are two different things. It’s important not to get these two ideas confused. The lifetime is the period during execution from when the variable is first created to when it is destroyed and the memory it occupies is freed for other uses. The scope of a variable is the region of program code over which the variable may be accessed. Following the variable definitions, the value of count1 is output to produce the first of the lines shown above. There is then a second brace, which starts a new block. Two variables, count1 and count2, are defined within this block, with values 20 and 30 respectively. The count1 declared here is different from the first count1. The first count1 still exists, but its name is masked by the second count1. Any use of the name count1 following the declaration within the inner block refers to the count1 declared within that block. I used a duplicate of the variable name count1 here only to illustrate what happens. Although this code is legal, it isn’t a good approach to programming in general. In a real-world programming it would be confusing, and if you use duplicate names makes, it very easy to hide variables defined in an outer scope accidentally. The value shown in the second output line shows that within the inner block, you are using the count1 in the inner scope — that is, inside the innermost braces: cout << “Value of inner count1 = “ << count1 << endl;
Had you still been using the outer count1, this would display the value 10. The variable count1 is then incremented by the statement: count1 += 3;
// This affects the inner count1
The increment applies to the variable in the inner scope, since the outer one is still hidden. However, count3, which was defined in the outer scope, is incremented in the next statement without any problem: count3 += count2;
This shows that the variables declared at the beginning of the outer scope are accessible from within the inner scope. (Note that if count3 had been declared after the second of the inner pair of braces, then it would still be within the outer scope, but in that case count3 would not exist when the above statement is executed.) After the brace ending the inner scope, count2 and the inner count1 cease to exist. The variables count1 and count3 are still there in the outer scope and the values displayed show that count3 was indeed incremented in the inner scope. If you uncomment the line: // cout << count2 << endl;
90
// uncomment to get an error
Data, Variables, and Calculations the program will no longer compile correctly, because it attempts to output a non-existent variable. You will get an error message something like c:\microsoft visual studio\myprojects\Ex2_07\Ex2_07.cpp(29) : error C2065: ‘count2’ : undeclared identifier
This is because count2 is out of scope at this point.
Positioning Variable Declarations You have great flexibility in where you place the declarations for your variables. The most important aspect to consider is what scope the variables need to have. Beyond that, you should generally place a declaration close to where the variable is to be first used in a program. You should write your programs with a view to making them as easy as possible for another programmer to understand, and declaring a variable at its first point of use can be helpful in achieving that. It is possible to place declarations for variables outside of all of the functions that make up a program. The next section looks at the effect that has on the variables concerned.
Global Variables Variables that are declared outside of all blocks and classes (I will discuss classes later in the book) are called globals and have global scope (which is also called global namespace scope or file scope). This means that they are accessible throughout all the functions in the file, following the point at which they are declared. If you declare them at the very top of your program, they will be accessible from anywhere in the file. Globals also have static storage duration by default. Global variables with static storage duration will exist from the start of execution of the program, until execution of the program ends. If you do not specify an initial value for a global variable, it will be initialized with 0 by default. Initialization of global variables takes place before the execution of main() begins, so they are always ready to be used within any code that is within the variable’s scope. Figure 2-12 shows the contents of a source file, Example.cpp, and the arrows indicate the scope of each of the variables. The variable value1, which appears at the beginning of the file, is declared at global scope, as is value4, which appears after the function main(). The scope of each global variable extends from the point at which it is defined to the end of the file. Even though value4 exists when execution starts, it cannot be referred to in main() because main() is not within the variable’s scope. For main() to use value4, you would need to move its declaration to the beginning of the file. Both value1 and value4 will be initialized with 0 by default, which is not the case for the automatic variables. Note that the local variable called value1 in function() hides the global variable of the same name.
91
Chapter 2 int main() { // Function scope starts here int count1 = 10; int count3 = 50; cout << endl << “Value of outer count1 = ” << count1; // New scope starts here... int count1 = 20; // This hides the outer count1 int count2 = 30; cout << endl << “Value of outer count1 = ” << count1; count2 count1 += 3; // This affects the inner count1 count3 += count 2; } // ...and ends here.
count3
{
count1
count1
cout << endl << “Value of outer count1 = ” << count1 << endl << “Value of outer count3 = ” count3;
}
// cout << endl << count2; // uncomment to get an error cout << endl; return 0; //Function scope ends here
Figure 2-12
Since global variables continue to exist for as long as the program is running, this might raise the question, “Why not make all variables global and avoid this messing about with local variables that disappear?” This sounds very attractive at first, but as with the Sirens of mythology, there are serious side effects that completely outweigh any advantages you may gain. Real programs are generally composed of a large number of statements, a significant number of functions, and a great many variables. Declaring all variables at the global scope greatly magnifies the possibility of accidental erroneous modification of a variable, as well as making the job of naming them sensibly quite intractable. They will also occupy memory for the duration of program execution. By keeping variables local to a function or a block, you can be sure they have almost complete protection from external effects, they will only exist and occupy memory from the point at which they are defined to the end of the enclosing block, and the whole development process becomes much easier to manage. If you take a look at the Class View pane on the right of the IDE window for any of the examples that you have created so far and extend the class tree for the project by clicking on the +, you will see an entry called Global Functions and Variables. If you click on this, you will see a list of everything in your program that has global scope. This will include all the global functions, as well as any global variables that you have declared.
92
Data, Variables, and Calculations Try It Out
The Scope Resolution Operator
As you have seen, a global variable can be hidden by a local variable with the same name. However, it’s still possible to get at the global variable by using the scope resolution operator (::), which you saw in Chapter 1 when I was discussing namespaces. I can demonstrate how this works with a revised version of the last example: // Ex2_08.cpp // Demonstrating variable scope #include using std::cout; using std::endl; int count1 = 100;
// Global version of count1
int main() { // Function scope starts here int count1 = 10; int count3 = 50; cout << endl << “Value of outer count1 = “ << count1 << endl; cout << “Value of global count1 = “ << ::count1 // From outer block << endl; {
// New scope starts here... int count1 = 20; //This hides the outer count1 int count2 = 30; cout << “Value of inner count1 = “ << count1 << endl; cout << “Value of global count1 = “ << ::count1 // From inner block << endl; count1 += 3; count3 += count2;
} cout << << << <<
// This affects the inner count1 // ...and ends here.
“Value of outer count1 = “ << count1 endl “Value of outer count3 = “ << count3 endl;
//cout << count2 << endl;
// uncomment to get an error
return 0; }
// Function scope ends here
If you compile and run this example, you’ll get the following output: Value of outer count1 = 10 Value of global count1 = 100 Value of inner count1 = 20
93
Chapter 2 Value of global count1 = 100 Value of outer count1 = 10 Value of outer count3 = 80
How It Works The shaded lines of code indicate the changes I have made to the previous example; I just need to discuss the effects of those. The declaration of count1 prior to the definition of the function main() is global, so in principle it is available anywhere through the function main(). This global variable is initialized with the value of 100: int count1 = 100;
// Global version of count1
However, you have two other variables called count1, which are defined within main(), so throughout the program the global count1 is hidden by the local count1 variables. The first new output statement is: cout << “Value of global count1 = “ << ::count1 << endl;
// From outer block
This uses the scope resolution operator (::) to make it clear to the compiler that you want to reference the global variable count1, not the local one. You can see that this works from the value displayed in the output. In the inner block, the global count1 is hidden behind two variables called count1: the inner count1 and the outer count1. We can see the global scope resolution operator doing its stuff within the inner block, as you can see from the output generated by the statement we have added there: cout << “Value of global count1 = “ << ::count1 << endl;
// From inner block
This outputs the value 100, as before — the long arm of the scope resolution operator used in this fashion always reaches a global variable. You have seen earlier that you can refer to a name in the std namespace by qualifying the name with the namespace name, such as with std::cout or std::endl. The compiler searches the namespace that has the name specified by the left operand of the scope resolution operator for the name that you specify as the right operand. In the preceding example, you are using the scope resolution operator to search the global namespace for the variable count1. By not specifying a namespace name in front of the operator, you are telling the compiler to search the global namespace for the name that follows it. You’ll be seeing a lot more of this operator when you get to explore object-oriented programming in Chapter 9, where it is used extensively.
Static Variables It’s conceivable that you might want to have a variable that’s defined and accessible locally, but which also continues to exist after exiting the block in which it is declared. In other words, you need to declare a variable within a block scope, but to give it static storage duration. The static specifier provides you with the means of doing this, and the need for this will become more apparent when we come to deal with functions in Chapter 5.
94
Data, Variables, and Calculations In fact, a static variable will continue to exist for the life of a program even though it is declared within a block and available only from within that block (or its sub-blocks). It still has block scope, but it has static storage duration. To declare a static integer variable called count you would write: static int count;
If you don’t provide an initial value for a static variable when you declare it, it will be initialized for you. The variable count declared here will be initialized with 0. The default initial value for a static variable is always 0, converted to the type applicable to the variable. Remember that this is not the case with automatic variables. If you don’t initialize your automatic variables, they will contain junk values left over from the program that last used the memory they occupy.
Namespaces I have mentioned namespaces several times, so it’s time you got a better idea of what they are about. They are not used in the libraries supporting MFC, but the libraries that support the CLR and Windows forms use namespaces extensively, and of course the ANSI C++ standard library does, too. You know already that all the names used in the ISO/ANSI C++ standard library are defined in a namespace with the name std. This means that all the names used in the standard library have an additional qualifying name, std, so cout for example is really std::cout. You can see fully qualified names in use this with a trivial example: // Ex2_09.cpp // Demonstrating namespace names #include int value = 0; int main() { std::cout << “enter an integer: “; std::cin >> value; std::cout << “\nYou entered “ << value << std:: endl; return 0; }
The declaration for the variable value is outside of the definition of main(). This declaration is said to be at global namespace scope, because the variable declaration does not appear within a namespace. The variable is accessible from anywhere within main() as well as from within any other function definitions that you might have in the same source file. I have put the declaration for value outside of main() just so I can demonstrate in the next section how it could be in a namespace. Note the absence of using declarations for cout and endl. It isn’t necessary in this case, because you are fully qualifying the names you are using from the namespace std. It would be silly to do so, but you could use cout as the name of the integer variable here, and there would be no confusion because cout
95
Chapter 2 by itself is different from std::cout. Thus namespaces provide a way to separate the names used in one part of a program from those used in another. This is invaluable with large project involving several teams of programmers working on different parts of the program. Each team can have its own namespace name, and worries about two teams accidentally using the same name for different functions disappear. Look at this line of code: using namespace std;
This statement is a using directive. The effect of this is to import all the names from the std namespace into the source file so you can refer to anything that is defined in this namespace without qualifying the name in you program. Thus you can write the name cout instead of std::cout and endl instead of std::endl. The downside of this blanket using directive is that it effectively negates the primary reason for using a namespace — that is, preventing accidental name clashes. The safest way to access names from a namespace is either to qualify each name explicitly with the namespace name; unfortunately, this tends to make the code very verbose and reduce its readability. The other possibility is to introduce just the names that you use in your code with using declarations, for example: using std::cout; using std::endl;
// Allows cout usage without qualification // Allows endl usage without qualification
These statements are called using declarations. Each statement introduces a single name from the specified namespace and allows it to be used unqualified within the program code that follows. This provides a much better way of importing names from a namespace as you only import the names that you actually use in your program. Because Microsoft has set the precedent of importing all names from the System namespace with C+/CLI code, I will continue with that in the C++/CLI examples. In general I recommend that you use using declarations in your own code rather than using directives when you are writing programs of any significant size. Of course, you can define your own namespace that has a name that you choose. The next section shows how that’s done.
Declaring a Namespace You use the keyword namespace to declare a namespace — like this: namespace myStuff { // Code that I want to have in the namespace myStuff... }
This defines a namespace with the name myStuff. All name declarations in the code between the braces will be defined within the myStuff namespace, so to access any such name from a point outside this namespace, the name must be qualified by the namespace name, myStuff, or have a using declaration that identifies that the name is from the myStuff namespace.
96
Data, Variables, and Calculations You can’t declare a namespace inside a function. It’s intended to be used the other way round; you use a namespace to contain functions, global variables, and other named entities such as classes in your program. You must not put the definition of main() in a namespace, though. The function main() where execution starts must always be at global namespace scope, otherwise the compiler won’t recognize it. You could put the variable value in the previous example in a namespace: // Ex2_10.cpp // Declaring a namespace #include namespace myStuff { int value = 0; } int main() { std::cout << “enter an integer: “; std::cin >> myStuff::value; std::cout << “\nYou entered “ << myStuff::value << std:: endl; return 0; }
The myStuff namespace defines a scope, and everything within the namespace scope is qualified with the namespace name. To refer to a name declared within a namespace from outside, you must qualify it with the namespace name. Inside the namespace scope any of the names declared within it can be referred to without qualification — they are all part of the same family. Now you must qualify the name value with myStuff, the name of our namespace. If not, the program will not compile. The function main() now refers to names in two different namespaces, and in general you can have as many namespaces in your program as you need. You could remove the need to qualify value by adding a using directive: // Ex2_11.cpp // Using a using directive #include namespace myStuff { int value = 0; } using namespace myStuff;
// Make all the names in myStuff available
int main() { std::cout << “enter an integer: “; std::cin >> value; std::cout << “\nYou entered “ << value << std:: endl; return 0; }
97
Chapter 2 You could also have a using directive for std as well, so you wouldn’t need to qualify standard library names either, but this is defeating the whole point of namespaces. Generally, if you use namespaces in your program, you should not add using directives all over your program; otherwise, you might as well not bother with namespaces in the first place. Having said that, we will add a using directive for std in all our examples to keep the code less cluttered and easier for you to read. When you are starting out with a new programming language, you can do without clutter, no matter how useful it is in practice.
Multiple Namespaces A real-world program is likely to involve multiple namespaces. You can have multiple declarations of a namespace with a given name and the contents of all namespace blocks with a given name are within the same namespace. For example, you might have a program file with two namespaces: namespace sortStuff { // Everything in here is within sortStuff namespace } namespace calculateStuff { // Everything in here is within calculateStuff namespace // To refer to names from sortStuff they must be qualified } namespace sortStuff { // This is a continuation of the namespace sortStuff // so from here you can refer to names in the first sortStuff namespace // without qualifying the names }
A second declaration of a namespace with a given name is just a continuation of the first, so you can reference names in the first namespace block from the second without having to qualify them. They are all in the same namespace. Of course, you would not usually organize a source file in this way deliberately, but in can arise quite naturally with header files that you include into a program. For example, you might have something like this: #include #include “myheader.h” #include <string>
// Contents are in namespace std // Contents are in namespace myStuff // Contents are in namespace std
// and so on...
Here, and string are ISO/ANSI C++ standard library headers, and myheader.h represents a header file that contains our program code. You have a situation with the namespaces that is an exact parallel of the previous illustration. This has given you a basic idea of how namespaces work. There is a lot more to namespaces than I have discussed here, but if you grasp this bit you should be able to find out more about it without difficulty, if the need arises.
98
Data, Variables, and Calculations Note that the two forms of #include directive in the previous code fragment cause the compiler to search for the file in different ways. When you specify the file to be included between angled brackets, you are indicating to the compiler that it should search for the file along the path specified by the /I compiler option, and failing that along the path specified by the INCLUDE environment variable. These paths locate the C+ library files, which is why this form is reserved for library headers. The INCLUDE environment variable points to the folder holding the library header and the /I option allows an additional directory containing library headers to be specified. When the file name is between double quotes, the compiler will search the folder that contains the file in which the #include directive appears. If the file is not found, it will search in any directories that #include the current file. If that fails to find the file, it will search the library directories.
C++/CLI Programming C++/CLI provides a number of extensions and additional capabilities to what I have discussed in this chapter up to now. I’ll first summarize these additional capabilities before going into details. The additional C++/CLI capabilities are: ❑
All of the ISO/ANSI fundamental data types can be used as I have described in a C++/CLI program, but they have some extra properties in certain contexts that I’ll come to.
❑
C++/CLI provides its own mechanism for keyboard input and output to the command line in a console program.
❑
C++/CLI introduces the safe_cast operator that ensures that a cast operation results in verifiable code being generated.
❑
C++/CLI provides an alternative enumeration capability that is class-based and offers more flexibility than the ISO/ANSI C++ enum declaration you have seen.
You’ll learn more about CLR reference class types beginning in Chapter 4, but because I have introduced global variables for native C++, I’ll mention now that variables of CLR reference class types cannot be global variables. I want to begin by looking at fundamental data types in C++/CLI.
C++/CLI Specific: Fundamental Data Types You can and should use the ISO/ANSI C++ fundamental data type names in your C++/CLI programs, and with arithmetic operations they work exactly as you have seen in native C++. In addition C++/CLI defines two additional integer types: Type
Bytes
Range of Values
long long
8
From 9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
unsigned long long
8
From 0 to 18,446,744,073,709,551,615
99
Chapter 2 To specify literals of type long long you append LL or lowercase ll to the integer value. For example: long long big = 123456789LL;
A literal of type unsigned long long you append ULL or ull to the integer value: unsigned long long huge = 999999999999999ULL;
Although all the operations with fundamental types you have seen work in the same way in C++/CLI, the fundamental type names in a C++/CLI program have a different meaning and introduces additional capabilities in certain situations. A fundamental type in a C++/CLI program is a value class type and can behave as an ordinary value or as an object if the circumstances require it. Within the C++/CLI language, each ISO/ANSI fundamental type name maps to a value class type that is defined in the System namespace. Thus, in a C++/CLI program, the ISO/ANSI fundamental type names are shorthand for the associated value class type. This enables value of a fundamental type to be treated simply as a value or automatically converted to an object of its associated value class type when necessary. The fundamental types, the memory they occupy, and the corresponding value class types are shown in the following table:
100
Fundamental Type
Size(bytes)
CLI Value Class
ool
1
System::Boolean
char
1
System::SByte
signed char
1
System::SByte
unsigned char
1
System::Byte
short
2
System::Int16
unsigned short
2
System::UInt16
int
4
System::Int32
unsigned int
4
System::UInt32
long
4
System::Int32
unsigned long
4
System::UInt32
long long
8
System::Int64
unsigned long long
8
System::UInt64
float
4
System::Single
double
8
System::Double
long double
8
System::Double
wchar_t
2
System::Char
Data, Variables, and Calculations By default, type char is equivalent to signed char so the associated value class type is System::SByte. Note that you can change the default for char to unsigned char by setting the compiler option /J, in which case the associated value class type will be System::Byte. System is the root namespace name in which the C++/CLI value class types are defined. There are many other types defined within the System namespace, such as the type String for representing strings that you’ll meet in Chapter 4. C++/CLI also defines the System::Decimal value class type within the System namespace and variables of type Decimal store exact decimal values with 28 decimal digits precision. As I said, the value class type associated with each fundamental type name adds important additional capabilities for such variables in C++/CLI. When necessary, the compiler will arrange for automatic conversions from the original value to the associated class type and vice versa; these processes are referred to as boxing and unboxing, respectively. This allows a variable of any of these types to behave as a simple value or as an object, depending on the circumstances. You’ll learn more about how and when this happens in Chapter 6. Because the ISO/ANSI C++ fundamental type names are aliases for the value class type names in a C++/CLI program, in principle you can use either in your C++/CLI code. For example, you already know you can write statements creating integer and floating-point variables like this: int count = 10; double value = 2.5;
You could use the value class names that correspond with the fundamental type names and have the program compile without any problem, like this: System::Int32 count = 10; System::Double value = 2.5;
While this is perfectly legal, you should use the fundamental type names such as int and double in your code, rather than the value class names System::Int32 and System::Double. The reason is that the mapping between fundamental type names and value class types I have described applies to the Visual C++ 2005 compiler; other compilers are not obliged to implement the same mapping. The fundamental type names are fixed by the C++/CLI language standard, but the mapping to value class types is implementation-dependent for most types. Type long in Visual C++ 2005 maps to type Int32, but it is quite possible that it could map to type Int64 on some other implementation. Having data of the fundamental types represented by objects of a value class type is an important feature of C++/CLI. In ISO/ANSI C++ fundamental types and class types are quite different, whereas in C++/CLR all data is stored as objects of a class type, either as a value class type or as a reference class type. You’ll learn about reference class types in Chapter 7. Next, you’ll try a CLR console program.
Try It Out
A Fruity CLR Console Program
Create a new project and select the project type as CLR and the template as CLR Console Application. You can then enter the project name as Ex2_12, as shown in Figure 2-13.
101
Chapter 2
Figure 2-13
When you click on the OK button, the Application Wizard will generate the project containing the following code: // Ex2_12.cpp : main project file. #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { Console::WriteLine(L”Hello World”); return 0; }
I’m sure you’ve noticed the extra stuff between the parentheses following main. This is concerned with passing values to the function main() when you initiate execution of the program from the command line, and you’ll learn more about this when you explore functions in detail. If you compile and execute the default project, it will write “Hello World” to the command line. Now, you’ll convert this program to a CLR version of Ex2_02 so you can see how similar it is. To do this, you can modify the code in Ex2_12.cpp as follows: // Ex2_12.cpp : main project file. #include “stdafx.h”
102
Data, Variables, and Calculations using namespace System; int main(array<System::String ^> ^args) { int apples, oranges; int fruit; apples = 5; oranges = 6; fruit = apples + oranges;
// Declare two integer variables // ...then another one // Set initial values // Get the total fruit
Console::WriteLine(L”\nOranges are not the only fruit...”); Console::Write(L”- and we have “); Console::Write(fruit); Console::Write(L” fruits in all.\n”); return 0; }
The new lines are shown shaded, and those in the lower block replace the two lines in the automatically generated version of main(). You can now compile and execute the project. The program should produce the following output: Oranges are not the only fruit... - and we have 11 fruits in all.
How It Works The only significant difference is in how the output is produced. The definitions for the variables and the computation are the same. Although you are using the same type names as in the ISO/ANSI C++ version of the example, the effect is not the same. The variables apples, oranges, and fruit will be of the C++/CLI type, System::Int32, that is specified by type int, and they have some additional capabilities compared to the ISO/ANSI type. The variables here can act as objects in some circumstances or as simple values as they do here. If you want to confirm that Int32 is the same as int in this case, you could replace the int type name with Int32 and recompile the example. It should work in exactly the same way. Evidently, the following line of code produces the first line of output: Console::WriteLine(L”\nOranges are not the only fruit...”);
The WriteLine() function is a C++/CLI function that is defined in the Console class in the System namespace. You’ll learn about classes in detail in Chapter 6, but for now the Console class represents the standard input and output streams that correspond to the keyboard and the command line in a command line window. Thus the WriteLine() function writes whatever is between the parentheses following the function name to the command line and then writes a newline character to move the cursor to the next line ready for the next output operation. Thus the preceding statement writes the text “\nOranges are not the only fruit...” between the double quotes to the command line. The L that precedes the string indicates that it is a wide-character string where each character occupies two bytes. The Write() function in the Console class is essentially the same as the WriteLine() function, the only difference being that it does not automatically write a newline character following the output that you specify. You can therefore use the Write() function when you want to write two or more items of data to the same line in individual output statements.
103
Chapter 2 Values that you place between the parentheses that follow the name of a function are called arguments. Depending on how a function was written, it will accept zero, one, or more arguments when it is called. When you need to supply more than one argument they must be separated by commas. There’s more to the output functions in the Console class, so I want to explore Write() and WriteLine() in a little more depth.
C++/CLI Output to the Command Line You saw in the previous example how you can use the Console::Write() and Console::WriteLine() methods to write a string or other items of data to the command line. You can put a variable of any of the types you have seen between the parentheses following the function name and the value will be written to the command line. For example, you could write the following statements to output information about a number of packages: int packageCount = 25; Console::Write(L”There are “); Console::Write(packageCount); Console::WriteLine(L” packages.”);
// // // //
Number of packages Write string - no newline Write value -no newline Write string followed by newline
Executing these statements will produce the output: There are 25 packages.
The output is all on the same line because the first two output statements use the Write() function, which does not output a newline character after writing the data. The last statement uses the WriteLine() function, which does write a newline after the output so any subsequent output will be on the next line. It looks a bit of a laborious process having to use three statements to write one line of output, and it will be no surprise that there is a better way. That capability is bound up with formatting the output to the command line in a .NET Framework program, so you’ll explore that next.
C++/CLI Specific — Formatting the Output Both the Console::Write() and Console::WriteLine() functions have a facility for you to control the format of the output, and the mechanism works in exactly the same way with both. The easiest way to understand it is through some examples. First, look at how you can get the output that was produced by the three output statements in the previous section with a single statement: int packageCount = 25; Console::WriteLine(L”There are {0} packages.”, packageCount);
The second statement here will output the same output as you saw in the previous section. The first argument to the Console::WriteLine() function here is the string L”There are {0} packages.”, and the bit that determines that the value of the second should be placed in the string is “{0}”. The braces enclose a format string that applies to the second argument to the function although in this instance the format string is about as simple as it could get, being just a zero. The arguments that follow the first argument to the Console::WriteLine() function are numbered in sequence starting with zero, like this:
104
Data, Variables, and Calculations referenced by: 0 1 2 etc. Console::WriteLine(“Format string”, arg2, arg3, arg4,... );
Thus the zero between the braces in the previous code fragment indicates that the value of the packageCount argument should replace the {0} in the string that is to be written to the command line. If you want to output the weight as well as the number of packages, you could write this: int packageCount = 25; double packageWeight = 7.5; Console::WriteLine(L”There are {0} packages weighing {1} pounds.”, packageCount, packageWeight);
The output statement now has three arguments, and the second and third arguments are referenced by 0 and 1, respectively, between the braces. So, this will produce the output: There are 25 packages weighing 7.5 pounds.
You could also write the statement with the last two arguments in reverse sequence, like this: Console::WriteLine(L”There are {1} packages weighing {0} pounds.”, packageWeight, packageCount);
The packageWeight variable is now referenced by 0 and packageCount by 1 in the format string, and the output will be the same as previously. You also have the possibility to specify how the data is to be presented on the command line. Suppose that you wanted the floating-point value packageWeight to be output with two places of decimals. You could do that with the following statement: Console::WriteLine(L”There are {0} packages weighing {1:F2} pounds.”, packageCount, packageWeight);
In the substring {1:F2}, the colon separates the index value, 1, that identifies the argument to be selected from the format specification that follows, F2. The F in the format specification indicates that the output should be in the form “±ddd.dd...” (where d represents a digit) and the 2 indicates that you want to have two decimal places after the point. The output produced by the statement will be: There are 25 packages weighing 7.50 pounds.
In general, you can write the format specification in the form {n,w : Axx} where the n is an index value selecting the argument following the format string, w is an optional field width specification, the A is a single letter specifying how the value should be formatted, and the xx is an optional one or two digits specifying the precision for the value. The field width specification is a signed integer. The value will be right-justified in the field if w is positive and left-justified when it is negative. If the value occupies less than the number of positions specified by w the output is padded with spaces; if the value requires more positions than that specified by w the width specification is ignored. Here’s another example: Console::WriteLine(L”Packages:{0,3} Weight: {1,5:F2} pounds.”, packageCount, packageWeight);
105
Chapter 2 The package count is output with a field width of 3 and the weight in a field width of 5, so the output will be: Packages: 25 Weight:
7.50 pounds.
There are other format specifiers that enable you to present various types of data in different ways. Here are some of the most useful format specifications: Format Specifier
Description
C or c
Outputs the value as a currency amount.
D or d
Outputs an integer as a decimal value. If you specify the precision to be more than the number of digits the number will be padded with zeroes to the left.
E or e
Outputs a floating-point value in scientific notation, that is, with an exponent. The precision value will indicate the number of digits to be output following the decimal point.
F or f
Outputs a floating-point value as a fixed-point number of the form ±dddd.dd. . . .
G or g
Outputs the value in the most compact form depending on the type of the value and whether you have specified the precision. If you don’t specify the precision, a default precision value will be used.
N or n
Outputs the value as a fixed-point decimal value using comma separators between each group of three digits when necessary.
X or x
Output an integer as a hexadecimal value. Upper of lowercase hexadecimal digits will be output depending on whether you specify X or x.
That gives you enough of a toehold in output to continue with more C++/CLI examples. Now you’ll take a quick look at some of this in action.
Try It Out
Formatted Output
Here’s an example that calculates the price of a carpet to demonstrate output in a CLR console program: // Ex2_13.cpp : main project file. // Calculating the price of a carpet #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { double carpetPriceSqYd = 27.95; double roomWidth = 13.5; // In feet double roomLength = 24.75; // In feet const int feetPerYard = 3;
106
Data, Variables, and Calculations double roomWidthYds = roomWidth/feetPerYard; double roomLengthYds = roomLength/feetPerYard; double carpetPrice = roomWidthYds*roomLengthYds*carpetPriceSqYd; Console::WriteLine(L”Room size is {0:F2} yards by {1:F2} yards”, roomLengthYds, roomWidthYds); Console::WriteLine(L”Room area is {0:F2} square yards”, roomLengthYds*roomWidthYds); Console::WriteLine(L”Carpet price is ${0:F2}”, carpetPrice); return 0; }
The output should be: Room size is 8.25 yards by 4.50 yards Room area is 37.13 square yards Carpet price is $1037.64 Press any key to continue . . .
How It Works The dimensions of the room are specified in feet whereas the carpet is priced per square yard so you have defined a constant, feetPerYard, to use in the conversion from feet to yards. In the expression to convert each dimension you are dividing a value of type double by a value of type int. The compiler will insert code to convert the value of type int to type double before carrying out the multiplication. After converting the room dimensions to yards you calculate the price of the carpet by multiplying the dimensions in yards to obtain the area in square yards and multiplying that by the price per square yard. The output statements use the F2 format specification to limit the output values to two decimal places. Without this there would be more decimal places in the output that would be inappropriate, especially for the price. You could try removing the format specification to see the difference. Note that the statement to output the area has an arithmetic expression as the second argument to the WriteLine() function. The compiler will arrange to first evaluate the expression, and then the result
will be passed as the actual argument to the function. In general, you can always use an expression as an argument to a function as long as the result of evaluating the expression is of a type that is consistent with the function parameter type.
C++/CLI Input from the Keyboard The keyboard input capabilities that you have with a .NET Framework console program are somewhat limited. You can read a complete line of input as a string using the Console::ReadLine() function, or you can read a single character using the Console::Read() function. You can also read which key was pressed using the Console::ReadKey() function. You would use the Console::ReadLine() function like this: String^ line = Console::ReadLine();
107
Chapter 2 This reads a complete line of input text that is terminated when you press the Enter key. The variable line is of type String^ and stores a reference to the string that results from executing the Console::ReadLine() function; the little hat character, ^, following the type name, String, indicates that this is a handle that references an object of type String. You’ll learn more about type String and handles for String objects in Chapter 4. A statement that reads a single character from the keyboard looks like this: char ch = Console::Read();
With the Read() function you could read input data character by character and then analyze the characters read and convert the input to a corresponding numeric value. The Console::ReadKey() function returns the key that was pressed as an object of type ConsoleKeyInfo, which is a value class type defined in the System namespace. Here’s a statement to read a key press: ConsoleKeyInfo keyPress = Console ReadKey(true);
The argument true to the ReadKey() function results in the key press not being displayed on the command line. An argument value of false (or omitting the argument) will cause the character corresponding the key pressed being displayed. The result of executing the function will be stored in keyPress. To identify the character corresponding to the key (or keys) pressed, you use the expression keyPress.KeyChar. Thus you could output a message relating to a key press with the following statement: Console::WriteLine(L”The key press corresponds to the character: {0}”, keyPress.KeyChar);
The key that was pressed is identified by the expression keyPress.Key. This expression refers to a value of a C++/CLI enumeration (which you’ll learn about very soon) that identifies the key that was pressed. There’s more to the ConsoleKeyInfo objects that I have described. You’ll meet it again later in the book. While not having formatted input in a C++/CLI console program is a slight inconvenience while you are learning, in practice this is a minor limitation. Virtually all the real-world programs you are likely to write will receive input through components of a window, so you won’t typically have the need to read data from the command line.
Using safe_cast The safe_cast operation is for explicit casts in the CLR environment. In most instances you can use static_cast to cast from one type to another in a C++/CLI program without problems, but because there are exceptions that will result in an error message, it is better to use safe_cast. You use safe_cast in exactly the same way as static_cast. For example: double value1 = 10.5; double value2 = 15.5; int whole_number = safe_cast(value1) + safe_cast(value2);
108
Data, Variables, and Calculations The last statement casts each on the values of type double to type int before adding them together and storing the result in whole_number.
C++/CLI Enumerations Enumerations in a C++/CLI program are significantly different from those in an ISO/ANSI C++ program. For a start you define an enumeration in C++/CLI them slightly differently: enum class Suit{Clubs, Diamonds, Hearts, Spades};
This defines an enumeration type, Suit, and variables of type Suit can be assigned only one of the values defined by the enumeration — Hearts, Clubs, Diamonds, or Spades. When you access the constants in a C++/CLI enumeration you must always qualify the constant you are using with the enumeration type name. For example: Suit suit = Suit::Clubs;
This statement assigns the value Clubs from the Suit enumeration to the variable with the name suit. The :: that separates the type name, Suit, from the name of the enumeration constant, Clubs, is the scope resolution operation and indicates that Clubs exists within the scope of the enumeration Suit. Note the use of the keyword class in the definition of the enumeration, following the enum keyword. This does not appear in the definition of an ISO/ANSI C++ enumeration as you saw earlier, and it identifies the enumeration as C++/CLI. It also gives a clue to another difference from an ISO/ANSI C++ enumeration; the constants here that are defined within the enumeration — Hearts, Clubs, and so on — are objects, not simply values of a fundamental type as in the ISO/ANSI C++ version. In fact by default they are objects of type Int32, so they each encapsulate a value of type int; however, you must cast the constant to type int before attempting to use it as such. Because a C++/CLI enumeration is a class type, you cannot define it locally, within a function for example, so if you want to define such an enumeration for use in main(), for example, you would define it at global scope. This is easy to see with an example.
Try It Out
Defining a C++/CLI Enumeration
Here’s a very simple example using an enumeration: // Ex2_14.cpp : main project file. // Defining and using a C++/CLI enumeration. #include “stdafx.h” using namespace System; // Define the enumeration at global scope enum class Suit{Clubs, Diamonds, Hearts, Spades}; int main(array<System::String ^> ^args)
109
Chapter 2 { Suit suit = Suit::Clubs; int value = safe_cast(suit); Console::WriteLine(L”Suit is {0} and suit = Suit::Diamonds; value = safe_cast(suit); Console::WriteLine(L”Suit is {0} and suit = Suit::Hearts; value = safe_cast(suit); Console::WriteLine(L”Suit is {0} and suit = Suit::Spades; value = safe_cast(suit); Console::WriteLine(L”Suit is {0} and return 0;
the value is {1} “, suit, value);
the value is {1} “, suit, value);
the value is {1} “, suit, value);
the value is {1} “, suit, value);
}
This example will produce the following output: Suit is Clubs and the value is 0 Suit is Diamonds and the value is 1 Suit is Hearts and the value is 2 Suit is Spades and the value is 3 Press any key to continue . . .
How It Works Because it is a class type, the Suit enumeration cannot be defined within the function main(), so its definition appears before the definition of main() and is therefore defined at global scope. The example defines a variable, suit, of type Suit and allocates the value Suit::Clubs to it initially with the statement: Suit suit = Suit::Clubs;
The qualification of the constant name Clubs with the type name Suit is essential; without it Clubs would not be recognized by the compiler. If you look at the output, the value of suit is displayed as the name of the corresponding constant — ”Clubs” in the first instance. To obtain the constant value that corresponds to the object, you must explicitly cast the value to type int as in the statement: value = safe_cast(suit);
You can see from the output that the enumeration constants have been assigned values starting from 0. In fact, you can change the type used for the enumeration constants. The next section looks at how that’s done.
110
Data, Variables, and Calculations Specifying a Type for Enumeration Constants The constants in a C++/CLI enumeration can be any of the following types: short
int
long
long long
signed char
char
unsigned short
unsigned int
unsigned long
unsigned long long
unsigned char
bool
To specify the type for the constants in an enumeration you write the type after the enumeration type name, but separated from it by a colon, just as with the native C++ enum. For example, to specify the enumeration constant type as char, you could write: enum class Face : char {Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King};
The constants in this enumeration will be of type Char and the underlying fundamental type will be type char. The first constant will correspond to code value 0 by default, and the subsequent values will be assigned in sequence. To get at the underlying value, you must explicitly cast the value to the type.
Specifying Values for Enumeration Constants You don’t have to accept the default for the underlying values. You can explicitly assign values to any or all of the constants defined by an enumeration. For example: enum class Face : char {Ace = 1, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King};
This will result in Ace having the value 1, Two having the value 2, an so on with King having the value 13. If you wanted the values to reflect the relative face card values with Ace high, you could write the enumeration as: enum class Face : char {Ace = 14, Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King};
In this case, Two will have the value 2, and successive constants will have values in sequence so King will still be 13. Ace will be 14, the value you have explicitly assigned. The values you assign to enumeration constants do not have to be unique. This provides the possibility of using the values of the constants to convey some additional property. For example: enum class WeekDays : bool { Mon =true, Tues = true, Wed = true, Thurs = true, Fri = true, Sat = false, Sun = false };
This defines the enumeration WeekDays where the enumeration constants are of type bool. The underlying values have been assigned to identify which represent work days as opposed to rest days.
111
Chapter 2
Summar y In this chapter, I have covered the basics of computation in C++. You have learned about all of the elementary types of data provided for in the language, and all the operators that manipulate these types directly. The essentials of what I have discussed up to now are as follows:
112
❑
A program in C++ consists of at least one function called main().
❑
The executable part of a function is made up of statements contained between braces.
❑
A statement in C++ is terminated by a semicolon.
❑
Named objects in C++, such as variables or functions, can have names that consist of a sequence of letters and digits, the first of which is a letter, and where an underscore is considered to be a letter. Upper- and lowercase letters are distinguished.
❑
All the objects, such as variables, that you name in your program must not have a name that coincides with any of the reserved words in C++. The full set of reserved words in C++ appears in Appendix A.
❑
All constants and variables in C++ are of a given type. The fundamental types in ISO/ANSI C++ are char, int, long, float, and double. C++/CLI also defines the types Int16, Int32, and Int64.
❑
The name and type of a variable is defined in a declaration statement ending with a semicolon. Variables may also be given initial values in a declaration.
❑
You can protect the value of a variable of a basic type by using the modifier const. This will prevent direct modification of the variable within the program and give you compiler errors everywhere that a constant’s value is altered.
❑
By default, a variable is automatic, which means that it exists only from the point at which it is declared to the end of the scope in which it is defined, indicated by the corresponding closing brace after its declaration.
❑
A variable may be declared as static, in which case it continues to exist for the life of the program. It can be accessed only within the scope in which it was defined.
❑
Variables can be declared outside of all blocks within a program, in which case they have global namespace scope. Variables with global namespace scope are accessible throughout a program, except where a local variable exists with the same name as the global variable. Even then, they can still be reached by using the scope resolution operator.
❑
A namespace defines a scope where each of the names declared within it are qualified by the namespace name. Referring to names from outside a namespace requires the names to be qualified.
❑
The ISO/ANSI C++ Standard Library contains functions and operators that you can use in your program. They are contained in the namespace std. The root namespace for C++/CLI libraries has the name System. Individual objects in a namespace can be accessed by using namespace name to qualify the object name by using the scope resolution operator, or you can supply a using declaration for a name from the namespace.
❑
An lvalue is an object that can appear on the left-hand side of an assignment. Non-const variables are examples of lvalues.
Data, Variables, and Calculations ❑
You can mix different types of variables and constants in an expression, but they will be automatically converted to a common type where necessary. Conversion of the type of the righthand side of an assignment to that of the left-hand side will also be made where necessary. This can cause loss of information when the left-hand side type can’t contain the same information as the right-hand side: double converted to int, or long converted to short, for example.
❑
You can explicitly cast the value of an expression to another type. You should always make an explicit cast to convert a value when the conversion may lose information. There are also situations where you need to specify an explicit cast in order to produce the result that you want.
❑
The keyword typedef allows you to define synonyms for other types.
Although I have discussed all the fundamental types, don’t be misled into thinking that’s all there is. There are more complex types based on the basic set as you’ll see, and eventually you will be creating original types of your own. From this chapter you can see there are three coding strategies you can adopt when writing a C++/CLI program: ❑
You should use the fundamental type names for variables but keep in mind that they are really synonyms for the value class type names in a C++/CLI program. The significance of this will be more apparent when you learn more about classes.
❑
You should use safe_cast and not static_cast in your C++/CLI code. The difference will be much more important in the context of casting class objects, but if you get into the habit of using safe_cast, generally you can be sure you will avoid problems.
❑
You should use enum class to declare enumeration types in C++/CLI.
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
Write an ISO/ANSI C++ program that asks the user to enter a number and then prints it out, using an integer as a local variable.
2.
Write a program that reads an integer value from the keyboard into a variable of type int, and uses one of the bitwise operators (i.e. not the % operator!) to determine the positive remainder when divided by 8. For example, 29 = (3x8)+5 and -14 = (-2x8)+2 have positive remainder 5 and 2, respectively.
3.
Fully parenthesize the following expressions, in order to show the precedence and associativity:
1 + 2 + 3 + 4 16 * 4 / 2 * 3 a > b? a: c > d? e: f a & b && c & d
113
Chapter 2 4.
Create a program that will calculate the aspect ratio of your computer screen, given the width and height in pixels, using the following statements: int width = 1280; int height = 1024; double aspect = width / height;
When you output the result, what answer will you get? Is it satisfactory — and if not, how could you modify the code, without adding any more variables?
5.
(Advanced) Without running it, can you work out what value the following code is going to output, and why? unsigned s = 555; int i = (s >> 4) & ~(~0 << 3); cout << i;
6.
Write a C++/CLI console program that uses an enumeration to identify months in the year with the values associated with the months running from 1 to 12. The program should output each enumeration constants and its underlying value.
7.
Write a C++/CLI program that will calculate the areas of three rooms to the nearest number of whole square feet that have the following dimensions in feet: Room1: 10.5 by 17.6 Room2: 12.7 by 18.9 Room3: 16.3 by 15.4 The program should also calculate and output the average area of the three rooms, and the total area, in each case the result should be to the nearest whole number of square feet.
114
3 Decisions and Loops In this chapter, you will look at how to add decision-making capabilities to your C++ programs. You’ll also learn how to make your programs repeat a set of actions until a specific condition is met. This will enable you to handle variable amounts of input, as well as make validity checks on the data that you read in. You will also be able to write programs that can adapt their actions depending on the input data and to deal with problems where logic is fundamental to the solution. By the end of this chapter, you will have learned: ❑
How to compare data values
❑
How to alter the sequence of program execution based on the result
❑
How to apply logical operators and expressions
❑
How to deal with multiple choice situations
❑
How to write and use loops in your programs
I’ll start with one of the most powerful and fundamental tools in programming: the ability to compare variables and expressions with other variables and expressions and, based on the outcome, execute one set of statements or another.
Comparing Values Unless you want to make decisions on a whim, you need a mechanism for comparing things. This involves some new operators called relational operators. Because all information in your computer is ultimately represented by numerical values (in the last chapter you saw how character information is represented by numeric codes), comparing numerical values is the essence of practically all decision making. You have six fundamental operators for comparing two values available: <
less than
<=
less than or equal to
>
greater than
>=
greater than or equal to
==
equal to
!=
not equal to
Chapter 3 The ‘equal to” comparison operator has two successive ‘=’ signs. This is not the same as the assignment operator, which consists only of a single ‘=’ sign. It’s a common mistake to use the assignment operator instead of the comparison operator, so watch out for this potential cause of confusion. Each of these operators compares the values of two operands and returns one of the two possible values of type bool: true if the comparison is true, or false if it is not. You can see how this works by having a look at a few simple examples of comparisons. Suppose you have created integer variables i and j with the values 10 and –5, respectively. The expressions, i>j
i != j
j > -8
i <= j + 15
all return the value true. Further assume that you have defined the following variables: char first = ‘A’, last = ‘Z’;
Here are some examples of comparisons using these character variables: first == 65 first < last
‘E’ <= first first != last
All four expressions involve comparing ASCII code values. The first expression returns true because first was initialized with ‘A’, which is the equivalent of decimal 65. The second expression checks whether the value of first, which is ‘A’, is less than the value of last, which is ‘Z’. If you check the ASCII codes for these characters in Appendix B, notice that the capital letters are represented by an ascending sequence of numerical values from 65 to 90, 65 representing ‘A’ and 90 representing ‘Z’, so this comparison also returns the value true. The third expression returns the value false because ‘E’ is greater than the value of first. The last expression returns true because ‘A’ is definitely not equal to ‘Z’. Consider some slightly more complicated numerical comparisons. With variables defined by the statements int i = -10, j = 20; double x = 1.5, y = -0.25E-10;
take a look at the following: -1 < y
j < (10 - i)
2.0*x >= (3 + y)
As you can see, you can use expressions that result in a numerical value as operands in comparisons. If you check with the precedence table for operators that you saw in Chapter 2, you see that none of the parentheses are strictly necessary, but they do help to make the expressions clearer. The first comparison is true and so returns the bool value true. The variable y has a very small negative value, -0.000000000025, and so is greater than -1. The second comparison returns the value false. The expression 10 - i has the value 20 which is the same as j. The third expression returns true because the expression 3 + y is slightly less than 3. You can use relational operators to compare values of any of the fundamental types, so all you need now is a practical way of using the results of a comparison to modify the behavior of a program.
116
Decisions and Loops
The if Statement The basic if statement allows your program to execute a single statement, or a block of statements enclosed within braces, if a given condition expression evaluates to the value true, or skip the statement or block of statements if the condition evaluates to false. This is illustrated in Figure 3-1. if( condition ) condition evaluates to true
{ condition evaluates to false
// Statements }
// More statements
Figure 3-1
A simple example of an if statement is: if(letter == ‘A’) cout << “The first capital, alphabetically speaking.”;
The condition to be tested appears in parentheses immediately following the keyword, if, and this is followed by the statement to be executed when the condition is true. Note the position of the semicolon here. It goes after the statement following the if and the condition between parentheses; there shouldn’t be a semicolon after the condition in parentheses because the two lines essentially make up a single statement. You also can see how the statement following the if is indented, to indicate that it is only executed when the if condition returns the value true. The indentation is not necessary for the program to execute, but it helps you to recognize the relationship between the if condition and the statement that depends on it. The output statement in the code fragment is executed only if the variable letter has the value ‘A’. You could extend this example to change the value of letter if it contains the value ‘A’: if(letter == ‘A’) { cout << “The first capital, alphabetically speaking.”; letter = ‘a’; }
The block of statements that is controlled by the if statement is delimited by the curly braces. Here you execute the statements in the block only if the condition (letter == ‘A’) evaluates to true. Without the braces, only the first statement would be the subject of the if, and the statement assigning the value ‘a’ to
117
Chapter 3 letter would always be executed. Note that there is a semicolon after each of the statements in the block,
not after the closing brace at the end of the block. There can be as many statements as you like within the block. Now, as a result of letter having the value ‘A’, you change its value to ‘a’ after outputting the same message as before. If the condition returns false, neither of these statements is executed.
Nested if Statements The statement to be executed when the condition in an if statement is true can also be an if. This arrangement is called a nested if. The condition for the inner if is only tested if the condition for the outer if is true. An if that is nested inside another can also contain a nested if. You can generally continue nesting ifs one inside the other like this for as long as you know what you are doing.
Try It Out
Using Nested Ifs
The following is the nested if with a working example. // Ex3_01.cpp // A nested if demonstration #include using std::cin; using std::cout; using std::endl; int main() { char letter = 0; cout << endl << “Enter a letter: “; cin >> letter;
// Store input in here
// Prompt for the input // then read a character
if(letter >= ‘A’) // Test for ‘A’ or larger if(letter <= ‘Z’) // Test for ‘Z’ or smaller { cout << endl << “You entered a capital letter.” << endl; return 0; } if(letter >= ‘a’) // Test for ‘a’ or larger if(letter <= ‘z’) // Test for ‘z’ or smaller { cout << endl << “You entered a small letter.” << endl; return 0; } cout << endl << “You did not enter a letter.” << endl; return 0; }
118
Decisions and Loops How It Works This program starts with the usual comment lines; then the #include statement for the header file supporting input/output and the using declarations for cin, cout, and endl that are the std namespace. The first action in the body of main() is to prompt for a letter to be entered. This is stored in the char variable with the name letter. The if statement that follows the input checks whether the character entered is ‘A’ or larger. Because the ASCII codes for lowercase letters (97 to 122) are greater than those for uppercase letters (65 to 90), entering a lowercase letter causes the program to execute the first if block, as (letter >= ‘A’) returns true for all letters. In this case, the nested if, which checks for an input of ‘Z’ or less, is executed. If it is ‘Z’ or less, you know that you have a capital letter, the message is displayed, and you are done, so you execute a return statement to end the program. Both statements are enclosed between braces, so they are both executed when the nested if condition returns true. The next if checks whether the character entered is lowercase, using essentially the same mechanism as the first if, displays a message and returns. If the character entered is not a letter, the output statement following the last if block is executed. This displays a message to the effect that the character entered was not a letter. The return is then executed. You can see that the relationship between the nested ifs and the output statement is much easier to follow because of the indentation applied to each. A typical output from this example is: Enter a letter: T You entered a capital letter.
You could easily arrange to change uppercase to lowercase by adding just one extra statement to the if, checking for uppercase: if(letter >= ‘A’) // Test for ‘A’ or larger if(letter <= ‘Z’) // Test for ‘Z’ or smaller { cout << endl << “You entered a capital letter.”; << endl; letter += ‘a’ - ‘A’;
// Convert to lowercase
return 0; }
This involves adding one additional statement. This statement for converting from uppercase to lowercase increments the letter variable by the value ‘a’ - ‘A’. It works because the ASCII codes for ‘A’ to ‘Z’ and ‘a’ to ‘z’ are two groups of consecutive numerical codes, so the expression ‘a’ - ‘A’ represents the value to be added to an uppercase letter to get the equivalent lowercase letter.
119
Chapter 3 You could equally well use the equivalent ASCII values for the letters here, but by using the letters you’ve ensured that this code would work on computers where the characters were not ASCII, as long as both the upper- and lowercase sets are represented by a contiguous sequence of numeric values. There is an ISO/ANSI C++ library function to convert letters to uppercase, so you don’t normally need to program for this yourself. It has the name toupper() and appears in the standard library file . You will see more about standard library facilities when you get to look specifically at how functions are written.
The Extended if Statement The if statement that you have been using so far executes a statement if the condition specified returns true. Program execution then continues with the next statement in sequence. You also have a version of the if that allows one statement to be executed if the condition returns true, and a different statement to be executed if the condition returns false. Execution then continues with the next statement in sequence. As you saw in Chapter 2, a block of statements can always replace a single statement, so this also applies to these ifs.
Try It Out
Extending the If
Here’s an extended if example. // Ex3_02.cpp // Using the extended if #include using std::cin; using std::cout; using std::endl; int main() { long number = 0; // Store input here cout << endl << “Enter an integer number less than 2 billion: “; cin >> number; if(number % 2L) // Test remainder after division by 2 cout << endl // Here if remainder 1 << “Your number is odd.” << endl; else cout << endl // Here if remainder 0 << “Your number is even.” << endl; return 0; }
120
Decisions and Loops Typical output from this program is: Enter an integer less than 2 billion: 123456 Your number is even,
How It Works After reading the input value into number, the value is tested by taking the remainder after division by two (using the remainder operator % that you saw in the last chapter) and using that as the condition for the if. In this case, the condition of the if statement returns an integer, not a Boolean. The if statement interprets a non-zero value returned by the condition as true, and interprets zero as false. In other words, the condition expression for the if statement: (number % 2L)
is equivalent to (number % 2L != 0)
If the remainder is 1, the condition is true, and the statement immediately following the if is executed. If the remainder is 0, the condition is false, and the statement following the else keyword is executed. In an if statement, the condition can be an expression that results in a value of any of the fundamental data types that you saw in Chapter 2. When the condition expression evaluates to a numerical value rather than the bool value required by the if statement, the compiler inserts an automatic cast of the result of the expression to type bool. A non-zero value that is cast to type bool results in true, and a zero value results in false. Because the remainder from the division of an integer by two can only be one or zero, I have commented the code to indicate this fact. After either outcome, the return statement is executed to end the program. The else keyword is written without a semicolon, similar to the if part of the statement. Again, indentation is used as a visible indicator of the relationship between various statements. You can clearly see which statement is executed for a true or non-zero result, and which for a false or zero result. You should always indent the statements in your programs to show their logical structure. The if-else combination provides a choice between two options. The general logic of the if-else is shown in Figure 3-2.
121
Chapter 3 if( condition ) condition evaluates to true
{ condition evaluates to false
// Statements }
else
{ // More statements }
// Even more statements
Figure 3-2
The arrows in the diagram indicate the sequence in which statements are executed, depending on whether the if condition returns true or false.
Nested if-else Statements As you have seen, you can nest if statements within if statements. You can also nest if-else statements within ifs, ifs within if-else statements, and if-else statements within if-else statements. This provides considerable room for confusion, so take a look at a few examples. The following is an example of an if-else nested within an if. if(coffee == ‘y’) if(donuts == ‘y’) cout << “We have coffee and donuts.”; else cout << “We have coffee, but not donuts”;
The test for donuts is executed only if the result of the test for coffee returns true, so the messages reflect the correct situation in each case; however, it is easy to get this confused. If you write much the same thing with incorrect indentation, you can be trapped into the wrong conclusion:
122
Decisions and Loops if(coffee == ‘y’) if(donuts == ‘y’) cout << “We have coffee and donuts.”; else // This else is indented incorrectly cout << “We have no coffee...”; // Wrong!
The mistake is easy to see here, but with more complicated if structures you need to keep in mind the rule about which if owns which else.
An else always belongs to the nearest preceding if that is not already spoken for by another else.
Whenever things look a bit complicated, you can apply this rule to sort things out. When you are writing your own programs you can always use braces to make the situation clearer. It isn’t really necessary in such a simple case, but you could write the last example as follows: if(coffee == ‘y’) { if(donuts == ‘y’) cout << “We have coffee and donuts.”; else cout << “We have coffee, but not donuts”; }
and it should be absolutely clear. Now that you know the rules, understanding the case of an if nested within an if-else becomes easy. if(coffee == ‘y’) { if(donuts == ‘y’) cout << “We have coffee and donuts.”; } else if(tea == ‘y’) cout << “We have tea, but not coffee”;
Here the braces are essential. If you leave them out, the else would belong to the if, which is looking out for donuts. In this kind of situation, it is easy to forget to include them and create an error that may be hard to find. A program with this kind of error compiles fine and even produces the right results some of the time. If you removed the braces in this example, you get the correct results only as long as coffee and donuts are both equal to ‘y’ so that the if(tea == ‘y’) check wouldn’t be executed. Here you’ll look at if-else statements nested in if-else statements. This can get very messy, even with just one level of nesting. if(coffee == ‘y’) if(donuts == ‘y’) cout << “We have coffee and donuts.”;
123
Chapter 3 else cout << “We have coffee, but not donuts”; else if(tea == ‘y’) cout << “We have no coffee, but we have tea, and maybe donuts...”; else cout << “No tea or coffee, but maybe donuts...”;
The logic here doesn’t look quite so obvious, even with the correct indentation. No braces are necessary, as the rule you saw earlier verifies that this is correct, but it would look a bit clearer if you included them. if(coffee == ‘y’) { if(donuts == ‘y’) cout << “We have coffee and donuts.”; else cout << “We have coffee, but not donuts”; } else { if(tea == ‘y’) cout << “We have no coffee, but we have tea, and maybe donuts...”; else cout << “No tea or coffee, but maybe donuts...”; }
There are much better ways of dealing with this kind of logic in a program. If you put enough nested ifs together, you can almost guarantee a mistake somewhere. The following section will help to simplify things.
Logical Operators and Expressions As you have just seen, using ifs where you have two or more related conditions can be a bit cumbersome. We have tried our iffy talents on looking for coffee and donuts, but in practice you may want to check much more complex conditions. You could be searching a personnel file for someone who is over 21, but under 35, female, with a college degree, unmarried, and speaks Hindi or Urdu. Defining a test for this could involve the mother of all ifs. Logical operators provide a neat and simple solution. Using logical operators, you can combine a series of comparisons into a single logical expression, so you end up needing just one if, virtually regardless of the complexity of the set of conditions, as long as the decision ultimately boils down to a choice between two possibilities(true or false. You have just three logical operators:
124
&&
Logical AND
||
Logical OR
!
Logical negation (NOT)
Decisions and Loops Logical AND You would use the AND operator, &&, where you have two conditions that must both be true for a true result. You want to be rich and healthy. For example, you could use the && operator when you are testing a character to determine whether it’s an uppercase letter; the value being tested must be both greater than or equal to ‘A’ AND less than or equal to ‘Z’. Both conditions must return true for the value to be a capital letter. As before, the conditions you combine using logical operators may return numerical values. Remember that in this case a non-zero value casts to the value true; zero casts to false. Taking the example of a value stored in a char variable letter, you could replace the test using two ifs for one that uses only a single if and the && operator: if((letter >= ‘A’) && (letter <= ‘Z’)) cout << “This is a capital letter.”;
The parentheses inside the expression that is the if condition ensure that there is no doubt that the comparison operations are executed first, which makes the statement clearer. Here, the output statement is executed only if both of the conditions that combined by the && operator are true. Just as with binary operators in the last chapter, you can represent the effect of a particular logical operator using a truth table. The truth table for && is as follows: &&
false
true
false
false
false
true
false
true
The row headings of the left and the column headings at the top represent the value of the logical expressions to be combined by the operator &&. Thus, to determine the result of combining a true condition with a false condition, select the row with true at the left and the column with false at the top and look at the intersection of the row and column for the result (false). Actually, you don’t really need a truth table because it’s very simple; the && operation results in the value true only if both operands are true.
Logical OR The OR operator, ||, applies when you have two conditions and you want a true result if either or both of them are true. For example, you might be considered creditworthy for a loan from the bank if your income was at least $100,000 a year, or you had $1,000,000 in cash. This could be tested using the following if. if((income >= 100000.00) || (capital >= 1000000.00)) cout << “How much would you like to borrow, Sir (grovel, grovel)?”;
The ingratiating response emerges when either or both of the conditions are true. (A better response might be, “Why do you want to borrow?” It’s strange how banks lend you money only if you don’t need it.)
125
Chapter 3 You can also construct a truth table for the || operator: ||
false
true
false
false
true
true
true
true
The result here can also be stated very simply: you only get a false result with the || operator when both operands are false.
Logical NOT The third logical operator, !, takes one operand of type bool and inverts its value. So if the value of a variable test is true, !test is false; and if test is false, !test is true. To take the example of a simple expression, if x has the value 10, the expression: !(x > 5)
is false, because x > 5 is true. You could also apply the ! operator in an expression that was a favorite of Charles Dickens: !(income > expenditure)
If this expression is true, the result is misery, at least as soon as the bank starts bouncing your checks. Finally, you can apply the ! operator to other basic data types. Suppose you have a variable, rate, that is of type float and has the value 3.2. For some reason you might want to test to verify that the value of rate is non-zero, in which case you could use the expression: !(rate)
The value 3.2 is non-zero and thus converts to the bool value true so the result of this expression is false.
Try It Out
Combining Logical Operators
You can combine conditional expressions and logical operators to any degree that you feel comfortable with. For example, you could construct a test for whether a variable contained a letter just using a single if. Let’s write it as a working example: // Ex3_03.cpp // Testing for a letter using logical operators #include using std::cin; using std::cout;
126
Decisions and Loops using std::endl; int main() { char letter = 0;
// Store input in here
cout << endl << “Enter a character: “; cin >> letter; if(((letter >= ‘A’) && (letter <= ‘Z’)) || ((letter >= ‘a’) && (letter <= ‘z’))) // Test for alphabetic cout << endl << “You entered a letter.” << endl; else cout << endl << “You didn’t enter a letter.” << endl; return 0; }
How It Works This example starts out in the same way as Ex3_01.cpp by reading a character after a prompt for input. The interesting part of the program is in the if statement condition. This consists of two logical expressions combined with the || (OR) operator, so that if either is true, the condition returns true and the message: You entered a letter. is displayed. If both logical expressions are false, the else statement is executed which displays the message: You didn’t enter a letter. Each of the logical expressions combines a pair of comparisons with the operator && (AND), so both comparisons must return true if the logical expression is to be true. The first logical expression returns true if the input is an uppercase letter, and the second returns true if the input is a lowercase letter.
The Conditional Operator The conditional operator is sometimes called the ternary operator because it involves three operands. It is best understood by looking at an example. Suppose you have two variables, a and b, and you want to assign the maximum of a and b to a third variable c. You can do this with the following statement: c = a > b ? a : b;
// Set c to the maximum of a and b
The first operand for the conditional operator must be an expression that results in a bool value, true or false, and in this case it is a > b. If this expression returns true, the second operand — in this case a — is selected as the value resulting from the operation. If the first argument returns false, the third
127
Chapter 3 operand — in this case b — is selected as the value that results from the operation. Thus, the result of the conditional expression a > b ? a : b is a if a is greater than b, and b otherwise. This value is stored in c as a result of the assignment operation. The use of the conditional operator in this assignment statement is equivalent to the if statement: if(a > b) c = a; else c = b;
The conditional operator can be written generally as: condition ? expression1 : expression2
If the condition evaluates as true, the result is the value of expression1, and if it evaluates to false, the result is the value of expression2.
Try It Out
Using the Conditional Operator with Output
A common use of the conditional operator is to control output, depending on the result of an expression or the value of a variable. You can vary a message by selecting one text string or another, depending on the condition specified. // Ex3_04.cpp // The conditional operator selecting output #include using std::cout; using std::endl; int main() { int nCakes = 1;
// Count of number of cakes
cout << endl << “We have “ << nCakes << “ cake” << ((nCakes > 1) ? “s.” : “.”) << endl; ++nCakes; cout << endl << “We have “ << nCakes << “ cake” << ((nCakes > 1) ? “s.” : “.”) << endl; return 0; }
The output from this program is: We have 1 cake. We have 2 cakes.
128
Decisions and Loops How It Works You first initialize the nCakes variable with the value 1; then you have an output statement that shows the number of cakes. The part that uses the conditional operator simply tests the variable to determine whether you have a singular cake or several cakes: ((nCakes>1) ? “s.” : “.”)
This expression evaluates to “s.” if nCakes is greater than 1, or “.” otherwise. This enables you to use the same output statement for any number of cakes and get grammatically correct output. You show this in the example by incrementing the nCakes variable and repeating the output statement. There are many other situations where you can apply this sort of mechanism; selecting between “is” and “are”, for example.
The switch Statement The switch statement enables you to select from multiple choices based on a set of fixed values for a given expression. It operates like a physical rotary switch in that you can select one of a fixed number of choices; some makes of washing machine provide a means of choosing an operation for processing your laundry in this way. There are a given number of possible positions for the switch, such as cotton, wool, synthetic fiber, and so on, and you can select any one of them by turning the knob to point to the option you want. In the switch statement, the selection is determined by the value of an expression that you specify. You define the possible switch positions by one or more case values, a particular one being selected if the value of the switch expression is the same as the particular case value. There is one case value for each possible choice in the switch and all the case values must be distinct. If the value of the switch expression does not match any of the case values, the switch automatically selects the default case. You can, if you want, specify the code for the default case, as you will do below; otherwise, the default is to do nothing.
Try It Out
The Switch Statement
You can examine how the switch statement works with the following example. // Ex3_05.cpp // Using the switch statement #include using std::cin; using std::cout; using std::endl; int main() { int choice = 0;
// Store selection value here
cout << endl
129
Chapter 3 << “Your electronic recipe book is at your service.” << endl << “You can choose from the following delicious dishes: “ << endl << endl << “1 Boiled eggs” << endl << “2 Fried eggs” << endl << “3 Scrambled eggs” << endl << “4 Coddled eggs” << endl << endl << “Enter your selection number: “; cin >> choice; switch(choice) { case 1: cout << endl << “Boil some eggs.” << endl; break; case 2: cout << endl << “Fry some eggs.” << endl; break; case 3: cout << endl << “Scramble some eggs.” << endl; break; case 4: cout << endl << “Coddle some eggs.” << endl; break; default: cout << endl <<”You entered a wrong number, try raw eggs.” << endl; } return 0; }
How It Works After defining your options in the stream output statement and reading a selection number into the variable choice, the switch statement is executed with the condition specified as simply choice in parentheses, immediately following the keyword switch. The possible options in the switch are enclosed between braces and are each identified by a case label. A case label is the keyword case, followed by the value of choice that corresponds to this option, and terminated by a colon. As you can see, the statements to be executed for a particular case are written following the colon at the end of the case label, and are terminated by a break statement. The break transfers execution to the statement after the switch. The break isn’t mandatory, but if you don’t include it, execution continues with the statements for the case that follows, which isn’t usually what you want. You can demonstrate this by removing the break statements from this example and seeing what happens. If the value of choice doesn’t correspond with any of the case values specified, the statements preceded by the default label are executed. A default case isn’t essential. In its absence, if the value of the test expression doesn’t correspond to any of the cases, the switch is exited and the program continues with the next statement after the switch.
Try It Out
Sharing a Case
Each of the expressions that you specify to identify the cases must be constant so that the value can be determined at compile time, and must evaluate to a unique integer value. The reason that no two case constants can be the same is that the compiler would have no way of knowing which case statement should be executed for that particular value; however, different cases don’t need to have a unique action. Several cases can share the same action, as shown here.
130
Decisions and Loops // Ex3_06.cpp // Multiple case actions #include using std::cin; using std::cout; using std::endl; int main() { char letter = 0; cout << endl << “Enter a small letter: “; cin >> letter; switch(letter*(letter >= ‘a’ && letter <= ‘z’)) { case ‘a’: case ‘e’: case ‘i’: case ‘o’: case ‘u’: cout << endl << “You entered a vowel.”; break; case 0: cout << endl << “That is not a small letter.”; break; default: cout << endl << “You entered a consonant.”; } cout << endl; return 0; }
How It Works In this example, you have a more complex expression in the switch. If the character entered isn’t a lowercase letter, the expression: (letter >= ‘a’ && letter <= ‘z’)
results in the value false; otherwise it evaluates to true. Because letter is multiplied by this expression, the value of the logical expression is cast to an integer(0 if the logical expression is false and 1 if it is true. Thus the switch expression evaluates to 0 if a lowercase letter was not entered and to the value of letter if it was. The statements following the case label case 0 are executed whenever the character code stored in letter does not represent a lowercase case letter. If a lowercase letter was entered, the switch expression evaluates to the same value as letter so for all values corresponding to vowels, the output statement following the sequence of case labels that have case values that are vowels. The same statement executes for any vowel because when any of these case labels is chosen successive statements are executed until the break statement is reached. You can see that a single action can be taken for a number of different cases by writing each of the case labels one after the other before the statements to be executed. If a lowercase letter that is a consonant is entered as program input, the default case label statement is executed.
131
Chapter 3
Unconditional Branching The if statement provides you with the flexibility to choose to execute one set of statements or another, depending on a specified condition, so the statement execution sequence is varied depending on the values of the data in the program. The goto statement, in contrast, is a blunt instrument. It enables you to branch to a specified program statement unconditionally. The statement to be branched to must be identified by a statement label which is an identifier defined according to the same rules as a variable name. This is followed by a colon and placed before the statement requiring labeling. Here is an example of a labeled statement. myLabel: cout << “myLabel branch has been activated” << endl;
This statement has the label myLabel, and an unconditional branch to this statement would be written as follows: goto myLabel;
Whenever possible, you should avoid using gotos in your program. They tend to encourage convoluted code that can be extremely difficult to follow. Because the goto is theoretically unnecessary in a program — there’s always an alternative approach to using goto — a significant cadre of programmers say you should never use it. I don’t subscribe to such an extreme view. It is a legal statement after all, and there are occasions when it can be convenient. I do, however, recommend that you only use it where you can see an obvious advantage over other options that are available; otherwise, you may end up with convoluted error-prone code that is hard to understand and even harder to maintain.
Repeating a Block of Statements The capability to repeat a group of statements is fundamental to most applications. Without this capability, an organization would need to modify the payroll program every time an extra employee was hired, and you would need to reload Halo 2 every time you wanted to play another game. So let’s first understand how a loop works.
What Is a Loop? A loop executes a sequence of statements until a particular condition is true (or false). You can actually write a loop with the C++ statements that you have met so far. You just need an if and the dreaded goto. Look at the following example. // Ex3_07.cpp // Creating a loop with an if and a goto #include using std::cin; using std::cout; using std::endl; int main()
132
Decisions and Loops { int i = 0, sum = 0; const int max = 10; i = 1; loop: sum += i; if(++i <= max) goto loop;
// Add current value of i to sum // Go back to loop until i = 11
cout << endl << “sum = “ << sum << endl << “i = “ << i << endl; return 0; }
This example accumulates the sum of integers from 1 to 10. The first time through the sequence of statements, i is 1 and is added to sum which starts out as zero. In the if, i is incremented to 2 and, as long as it is less than or equal to max, the unconditional branch to loop occurs and the value of i, now 2, is added to sum. This continues with i being incremented and added to sum each time, until finally, when i is incremented to 11 in the if, the branch back is not executed. If you run this example, you get the following output. sum = 55 i = 11
This shows quite clearly how the loop works; however, it uses a goto and introduces a label into the program, both of which you should avoid if possible. You can achieve the same thing, and more, with the next statement, which is specifically for writing a loop.
Try It Out
Using the for Loop
You can rewrite the last example using what is known as a for loop. // Ex3_08.cpp // Summing integers with a for loop #include using std::cin; using std::cout; using std::endl; int main() { int i = 0, sum = 0; const int max = 10; for(i = 1; i <= max; i++) sum += i;
// Loop specification // Loop statement
cout << endl
133
Chapter 3 << “sum = “ << sum << endl << “i = “ << i << endl; return 0; }
How It Works If you compile and run this, you get exactly the same output as the previous example, but the code is much simpler here. The conditions determining the operation of the loop appear in parentheses after the keyword for. There are three expressions that appear within the parentheses separated by semicolons: ❑
The first expression executes once at the outset and sets the initial conditions for the loop. In this case, it sets i to 1
❑
The second expression is a logical expression that determines whether the loop statement (or block of statements) should continue to be executed. If the second expression is true, the loop continues to execute; when it is false, the loop ends and execution continues with the statement that follows the loop. In this case the loop statement on the following line is executed as long as i is less than or equal to max
❑
The third expression is evaluated after the loop statement (or block of statements) executes, and in this case increments i each iteration. After this expression has been evaluated the second expression is evaluated once more to see whether the loop should continue.
Actually, this loop is not exactly the same as the version in Ex3_07.cpp. You can demonstrate this if you set the value of max to 0 in both programs and run them again;p then, you will find that the value of sum is 1 in Ex3_07.cpp and 0 in Ex3_08.cpp, and the value of i differs too. The reason for this is that the if version of the program always executes the loop at least once because you don’t check the condition until the end. The for loop doesn’t do this, because the condition is actually checked at the beginning. The general form of the for loop is: for (initializing_expression ; test_expression ; increment_expression) loop_statement;
Of course, loop_statement can be a single statement or a block of statements between braces. The sequence of events in executing the for loop is shown in Figure 3-3. As I have said, the loop statement shown in Figure 3-3 can also be a block of statements. The expressions controlling the for loop are very flexible. You can even write two or more expressions separated by the comma operator for each control expression. This gives you a lot of scope in what you can do with a for loop.
134
Decisions and Loops Execute initializing_expression
test_expression evaluates to true?
No
Yes
Execute loop_statement
Execute increment_expression
Continue with next statement
Figure 3-3
Variations on the for Loop Most of the time, the expressions in a for loop are used in a fairly standard way: the first for initializing one or more loop counters, the second to test if the loop should continue, and the third to increment or decrement one or more loop counters. You are not obliged to use these expressions in this way, however, and quite a few variations are possible. The initialization expression in a for loop can also include a declaration for a loop variable. In the previous example you could have written the loop to include the declaration for the loop counter I in the first control expression. for(int i = 1; i <= max; i++) sum += i;
// Loop specification // Loop statement
135
Chapter 3 Naturally, the original declaration for i would need to be omitted in the program. If you make this change to the last example, you will find that it now does not compile because the loop variable, i, ceases to exist after the loop so you cannot refer to it in the output statement. A loop has a scope which extends from the for expression to the end of the body of the loop, which of course can be a block of code between braces, as well as just a single statement. The counter i is now declared within the loop scope, so you cannot refer to it in the output statement because this is outside the scope of the loop. By default the C++ compiler enforces the ISO/ANSI C++ standard by insisting that a variable defined within a loop condition cannot be reference outside the loop. If you need to use the value in the counter after the loop has executed, you must declare the counter variable outside the scope of the loop. You can omit the initialization expression altogether from the loop. If you initialize i appropriately in the declaration statement for it, you can write the loop as: int i = 1; for(; i <= max; i++) sum += i;
// Loop specification // Loop statement
You still need the semicolon that separates the initialization expression from the test condition for the loop. In fact, both semicolons must always be present regardless of whether any or all of the control expressions are omitted. If you omit the first semicolon, the compiler is unable to decide which expression has been omitted or even which semicolon is missing. The loop statement can be empty. For example, you could place the loop statement in for loop from the previous example inside the increment expression; in this case the loop becomes for(i = 1; i <= max; sum += i++);
// The whole loop
You still need the semicolon after the closing parentheses, to indicate that the loop statement is now empty. If you omit this, the statement immediately following this line is interpreted as the loop statement. Sometime you’ll see the empty loop statement written on a separate line like the following. for(i = 1; i <= max; sum += i++) ;
Try It Out
// The whole loop
Using Multiple Counters
You can use the comma operator to include multiple counters in a for loop. You can see this in operation in the following program. // Ex3_09.cpp // Using multiple counters to show powers of 2 #include #include using using using using
std::cin; std::cout; std::endl; std::setw;
int main() {
136
Decisions and Loops long i = 0, power = 0; const int max = 10; for(i = 0, power = 1; i <= max; i++, power += power) cout << endl << setw(10) << i << setw(10) << power; // Loop statement cout << endl; return 0; }
How It Works You initialize two variables in the initialization section of the for loop, separated by the comma operator, and increment each of them in the increment section. Clearly, you can put as many expressions as you like in each position. You can even specify multiple conditions, separated by commas, in second expression that represents the test part of the for loop that determines whether it should continue; but only the right-most condition affect when the loop ends. Note that the assignments defining the initial values for i and power are expressions, not statements. A statement always ends with a semicolon. For each increment of i, the value of the variable power is doubled by adding it to itself. This produces the powers of two that we are looking for and so the program produces the following output. 0 1 2 3 4 5 6 7 8 9 10
1 2 4 8 16 32 64 128 256 512 1024
The setw() manipulator that you saw in the previous chapter is used to align the output nicely. You have included header file and added a using declaration for the name in the std namespace so we can use setw() without qualifying the name
Try It Out
The Infinite for Loop
If you omit the second control expression that specifies the test condition for a for loop, the value is assumed to be true, so the loop continues indefinitely unless you provide some other means of exiting from it. In fact, if you like, you can omit all the expressions in the parentheses after for. This may not seem to be useful; in fact, however, quite the reverse is true. You will often come across situations where you want to execute a loop a number of times, but you do not know in advance how many iterations you will need. Have a look at the following.
137
Chapter 3 // Ex3_10.cpp // Using an infinite for loop to compute an average #include using std::cin; using std::cout; using std::endl; int main() { double value = 0.0; double sum = 0.0; int i = 0; char indicator = ‘n’; for(;;) { cout << endl << “Enter a value: “; cin >> value; ++i; sum += value;
// // // //
Value entered stored here Total of values accumulated here Count of number of values Continue or not?
// Infinite loop
// Read a value // Increment count // Add current input to total
cout << endl << “Do you want to enter another value (enter n to end)? “; cin >> indicator; // Read indicator if ((indicator == ‘n’) || (indicator == ‘N’)) break; // Exit from loop } cout << endl << “The average of the “ << i << “ values you entered is “ << sum/i << “.” << endl; return 0; }
How It Works This program computes the average of an arbitrary number of values. After each value is entered, you need to indicate whether you want to enter another value, by entering a single character y or n. Typical output from executing this example is: Enter a value: 10 Do you want to enter another value (enter n to end)? y Enter a value: 20 Do you want to enter another value (enter n to end)? y Enter a value: 30
138
Decisions and Loops Do you want to enter another value (enter n to end)? n The average of the 3 values you entered is 20.
After declaring and initializing the variables that you’re going to use, you start a for loop with no expressions specified, so there is no provision for ending it here. The block immediately following is the subject of the loop that is to be repeated. The loop block performs three basic actions: ❑
It reads a value
❑
It adds the value read from cin to sum
❑
It checks whether you want to continue to enter values
The first action within the block is to prompt you for input and then read a value into the variable value. The value that you enter is added to sum and the count of the number of values, i, is incremented. After accumulating the value in sum, you are asked if you want to enter another value and prompted to enter ‘n’ if you have finished. The character that you enter is stored in the variable indicator for testing against ‘n’ or ‘N’ in the if statement. If neither is found, the loop continues; otherwise, a break is executed. The effect of break in a loop is similar to its effect in the context of the switch statement. In this instance, it exits the loop immediately by transferring control to the statement following the closing brace of the loop block. Finally, you output the count of the number of values entered and their average, which is calculated by dividing sum by i. Of course, i is promoted to type double before the calculation, as you remember from the casting discussion in Chapter 2.
Using the continue Statement There is another statement, besides break, used to affect the operation of a loop: the continue statement. This is written simply as: continue;
Executing continue within a loop starts the next loop iteration immediately, skipping over any statements remaining in the current iteration. I can demonstrate how this works with the following code. #include using std::cin; using std::cout; using std::endl; int main() { int i = 0, value = 0, product = 1; for(i = 1; i <= 10; i++) { cout << “Enter an integer: “;
139
Chapter 3 cin >> value; if(value == 0) continue;
// If value is zero // skip to next iteration
product *= value; } cout << “Product (ignoring zeros): “ << product << endl; return 0;
// Exit from loop
}
This loop reads 10 values with the intention of producing the product of the values entered. The if checks each value entered, and if it is zero, the continue statement skips to the next iteration. This is so that you don’t end up with a zero product if one of the values is zero. Obviously, if a zero value occurred on the last iteration, the loop would end. There are clearly other ways of achieving the same result, but continue provides a very useful capability, particularly with complex loops where you may need to skip to the end of the current iteration from various points in the loop. The effect of the break and continue statements on the logic of a for loop is illustrated in Figure 3-4. Obviously, in a real situation, you would use the break and continue statements with some conditiontesting logic to determine when the loop should be exited, or when an iteration of the loop should be skipped. You can also use the break and continue statements with the other kinds of loop, which I’ll discuss later on in this chapter, where they work in exactly the same way.
Try It Out
Using Other Types in Loops
So far, you have only used integers to count loop iterations. You are in no way restricted as to what type of variable you use to count iterations. Look at the following example. // Ex3_11.cpp // Display ASCII codes for alphabetic characters #include #include using using using using using
std::cout; std::endl; std::hex; std::dec; std::setw;
int main() { for(char capital = ‘A’, small = ‘a’; capital <= ‘Z’; capital++, small++) cout << endl << “\t” << capital // Output capital as a character << hex << setw(10) << static_cast(capital) // and as hexadecimal
140
Decisions and Loops << << << <<
dec << setw(10) << static_cast(capital) “ “ << small // Output hex << setw(10) << static_cast(small) dec << setw(10) << static_cast(small);
// and as decimal small as a character // and as hexadecimal // and as decimal
cout << endl; return 0; }
Execute initializing_expression
test_expression evaluates to true?
No
Yes
continue;
break; Succeeding statements in the loop are omitted
Execute increment_expression The loop is exited directly
Continue with next statement
Figure 3-4
141
Chapter 3 How It Works You have using declarations for the names of some new manipulators that are used in the program to affect how the output is presented. The loop in this example is controlled by the char variable capital, which you declare along with the variable small in the initializing expression. You also increment both variables in the third control expression for the loop so that the value of capital varies from ‘A’ to ‘Z’, and the value of small correspondingly varies from ‘a’ to ‘z’. The loop contains just one output statement spread over seven lines. The first line is: cout << endl
This starts a new line on the screen. The next three lines are: << “\t” << capital // Output capital as a character << hex << setw(10) << static_cast(capital) // and as hexadecimal << dec << setw(10) << static_cast(capital) // and as decimal
On each iteration, after outputting a tab character, the value of capital is displayed three times: as a character, as a hexadecimal value, and as a decimal value. When you insert the manipulator hex into the cout stream, this causes subsequent data values to be displayed as hexadecimal values rather than the default decimal representation for integer values so the second output of capital is as a hexadecimal representation of the character code. You then insert the dec manipulator into the stream to cause succeeding values to be output as decimal once more. By default a variable of type char is interpreted by the stream as a character, not a numerical value. You get the char variable capital to output as a numerical value by casting its value to type int, using the static_cast<>() operator that you saw in the previous chapter. The value of small is output in a similar way by the next three lines of the output statement: << “ “ << small // Output small as a character << hex << setw(10) << static_cast(small) // and as hexadecimal << dec << setw(10) << static_cast(small); // and as decimal
As a result, the program generates the following output: A B C D E F G H I
142
41 42 43 44 45 46 47 48 49
65 66 67 68 69 70 71 72 73
a b c d e f g h i
61 62 63 64 65 66 67 68 69
97 98 99 100 101 102 103 104 105
Decisions and Loops J K L M N O P Q R S T U V W X Y Z
4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
j k l m n o p q r s t u v w x y z
6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
Floating-Point Loop Counters You can also use a floating-point value as a loop counter. Here’s an example of a for loop with this kind of counter: double a = for(double cout << <<
0.3, b = x = 0.0; “\n\tx = “\ta*x +
2.5; x <= 2.0; x += 0.25) “ << x b = “ << a*x + b;
This code fragment calculates the value of a*x+b for values of x from 0.0 to 2.0 in steps of 0.25; however, you need to take care when using a floating-point counter in a loop. Many decimal values cannot be represented exactly in binary floating-point form, so discrepancies can build up with accumulative values. This means that you should not code a for loop such that ending the loop depends on a floating-point loop counter reaching a precise value. For example, the following poorly designed loop never ends. for(double x = 0.0 ; x != 1.0 ; x += 0.2) cout << x;
The intention with this loop is to output the value of x as it varies from 0.0 to 1.0; however, 0.2 has no exact representation as a binary floating-point value so the value of x is never exactly 1. Thus the second loop control expression is always false and so the loop continues indefinitely.
The while Loop A second kind of loop in C++ is the while loop. Where the for loop is primarily used to repeat a statement or a block for a prescribed number of iterations, the while loop is used to execute a statement or block of statements as long as a specified condition is true. The general form of the while loop is: while(condition) loop_statement;
143
Chapter 3 Here loop_statement is executed repeatedly as long as the condition expression has the value true. After the condition becomes false, the program continues with the statement following the loop. As always, a block of statements between braces could replace the single loop_statement. The logic of the while loop can be represented as shown in Figure 3-5.
while loop logic
loop condition evaluates to true?
No
Yes
loop_statement;
Continue with next statement
Figure 3-5
Try It Out
Using the while Loop
You could rewrite the earlier example that computes averages (Ex3_10.cpp) to use the while loop. // Ex3_12.cpp // Using a while loop to compute an average #include using std::cin; using std::cout; using std::endl; int main() { double value = 0.0; double sum = 0.0; int i = 0; char indicator = ‘y’; while(indicator == ‘y’)
144
// // // //
Value entered stored here Total of values accumulated here Count of number of values Continue or not?
// Loop as long as y is entered
Decisions and Loops { cout << endl << “Enter a value: “; cin >> value; ++i; sum += value;
// Read a value // Increment count // Add current input to total
cout << endl << “Do you want to enter another value (enter n to end)? “; cin >> indicator; // Read indicator } cout << endl << “The average of the “ << i << “ values you entered is “ << sum/i << “.” << endl; return 0; }
How It Works For the same input, this version of the program produces the same output as before. One statement has been updated, and another has been added — they are highlighted in the previous code. The for loop statement has been replaced by the while statement, and the test for indicator in the if has been deleted, as this function is performed by the while condition. You have to initialize indicator with ‘y’ in place of the ‘n’ which appeared previously — otherwise the while loop terminates immediately. As long as the condition in the while returns true, the loop continues. You can put any expression resulting in true or false as a while loop condition. The example would be a better program if the loop condition were extended to allow ‘Y’ to be entered to continue the loop as well as ‘y’. You could modify the while to the following to do the trick. while((indicator == ‘y’) || (indicator == ‘Y’))
You can also create a while loop that potentially executes indefinitely by using a condition that is always true. This can be written as follows. while(true) { ... }
You could also write the loop control expression as the integer value 1, which would be converted to the bool value true. Naturally, the same requirement applies here as in the case of the infinite for loop: namely, that you must provide some way of exiting the loop within the loop block. You’ll see other ways to use the while loop in Chapter 4.
145
Chapter 3
The do-while Loop The do-while loop is similar to the while loop in that the loop continues as long as the specified loop condition remains true. The main difference is that the condition is checked at the end of the loop — which contrasts with the while loop and the for loop where the condition is checked at the beginning of the loop. Consequently, the do-while loop statement is always executed at least once. The general form of the do-while loop is: do { loop_statements; }while(condition);
The logic of this form of loop is shown in Figure 3-6.
loop_statement;
Yes
loop condition evaluates to true? No
Continue with next statement
Figure 3-6
You could replace the while loop in the last version of the program to calculate an average with a do-while loop: do { cout << endl << “Enter a value: “; cin >> value; ++i; sum += value;
// Read a value // Increment count // Add current input to total
cout << “Do you want to enter another value (enter n to end)?”; cin >> indicator; // Read indicator } while((indicator == ‘y’) || (indicator == ‘Y’));
146
Decisions and Loops There’s little to choose between the two versions of the loop, except that this version doesn’t depend on the initial value set in indicator for correct operation. As long as you want to enter at least one value, which is not unreasonable for the calculation in question, this version of the loop is preferable.
Nested Loops You can nest one loop inside another. In Chapter 4, the usual application of this will become more apparent — it’s typically applied to repeating actions at different levels of classification. An example might be calculating the total marks for each student in a class and then repeating the process for each class in a school.
Try It Out
Nested Loops
You can see the effects of nesting one loop inside another by calculating the values of a simple formula. A factorial of an integer is the product of all the integers from 1 to the integer in question; so the factorial of 3, for example, is 1 times 2 times 3, which is 6. The following program computes the factorial of integers that you enter (until you’ve had enough): // Ex3_13.cpp // Demonstrating nested loops to compute factorials #include using std::cin; using std::cout; using std::endl; int main() { char indicator = ‘n’; long value = 0, factorial = 0; do { cout << endl << “Enter an integer value: “; cin >> value; factorial = 1; for(int i = 2; i <= value; i++) factorial *= i; cout << “Factorial “ << value << “ is “ << factorial; cout << endl << “Do you want to enter another value (y or n)? “; cin >> indicator; } while((indicator == ‘y’) || (indicator == ‘Y’)); return 0; }
147
Chapter 3 If you compile and execute this example, the typical output produced is: Enter an integer value: 5 Factorial 5 is 120 Do you want to enter another value (y or n)? y Enter an integer value: 10 Factorial 10 is 3628800 Do you want to enter another value (y or n)? y Enter an integer value: 13 Factorial 13 is 1932053504 Do you want to enter another value (y or n)? y Enter an integer value: 22 Factorial 22 is -522715136 Do you want to enter another value (y or n)? n
How It Works Factorial values grow very fast. In fact, 12 is the largest input value for which this example produces a correct result. The factorial of 13 is actually 6,227,020,800, not 1,932,053,504 as the program tells you. If you run it with even larger input values, leading digits are lost in the result stored in the variable factorial, and you may well get negative values for the factorial as you do when you ask for the factorial of 22. This situation doesn’t cause any error messages, so it is of paramount importance that you are sure that the values you’re dealing with in a program can be contained in the permitted range of the type of variable you’re using. You also need to consider the effects of incorrect input values. Errors of this kind, which occur silently, can be very hard to find. The outer of the two nested loops is the do-while loop, which controls when the program ends. As long as you keep entering y or Y at the prompt, the program continues to calculate factorial values. The factorial for the integer entered is calculated in the inner for loop. This is executed value times to multiply the variable factorial (with an initial value of 1) with successive integers from 2 to value.
Try It Out
Another Nested Loop
Nested loops can be a little confusing, so let’s try another example. This program generates a multiplication table of a given size. // Ex3_14.cpp // Using nested loops to generate a multiplication table #include #include using std::cout; using std::endl; using std::setw; int main() { const int size = 12;
148
// Size of table
Decisions and Loops int i = 0, j = 0;
// Loop counters
cout << endl // Output table title << size << “ by “ << size << “ Multiplication Table” << endl << endl; cout << endl << “ |”; for(i = 1; i <= size; i++) cout << setw(3) << i << “
// Loop to output column headings “;
cout << endl; for(i = 0; i <= size; i++) cout << “_____”; for(i = 1; i <= size; i++) { cout << endl << setw(3) << i << “ |”; for(j = 1; j <= size; j++) cout << setw(3) << i*j << “ } cout << endl; return 0;
// Newline for underlines // Underline each heading // Outer loop for rows
// Output row label
“;
// Inner loop for the rest of the row // End of inner loop
// End of outer loop
}
The output from this example is: 12 by 12 Multiplication Table
| 1 2 3 4 5 6 7 8 9 10 11 12 _________________________________________________________________ 1 | 1 2 3 4 5 6 7 8 9 10 11 12 2 | 2 4 6 8 10 12 14 16 18 20 22 24 3 | 3 6 9 12 15 18 21 24 27 30 33 36 4 | 4 8 12 16 20 24 28 32 36 40 44 48 5 | 5 10 15 20 25 30 35 40 45 50 55 60 6 | 6 12 18 24 30 36 42 48 54 60 66 72 7 | 7 14 21 28 35 42 49 56 63 70 77 84 8 | 8 16 24 32 40 48 56 64 72 80 88 96 9 | 9 18 27 36 45 54 63 72 81 90 99 108 10 | 10 20 30 40 50 60 70 80 90 100 110 120 11 | 11 22 33 44 55 66 77 88 99 110 121 132 12 | 12 24 36 48 60 72 84 96 108 120 132 144
How It Works The table title is produced by the first output statement in the program. The next output statement, combined with the loop following it, generates the column headings. Each column is five characters wide, so the heading value is displayed in a field width of three specified by the setw(3) manipulator, followed by two blanks. The output statement preceding the loop outputs four spaces and a vertical bar above the first column which contains the row headings. A series of underline characters is then displayed beneath the column headings.
149
Chapter 3 The nested loop generates the main table contents. The outer loop repeats once for each row, so i is the row number. The output statement cout << endl << setw(3) << i << “ |”;
// Output row label
goes to a new line for the start of a row and then outputs the row heading given by the value of i in a field width of three, followed by a space and a vertical bar. A row of values is generated by the inner loop: for(j = 1; j <= size; j++) cout << setw(3) << i*j << “
“;
// Inner loop for the rest of the row // End of inner loop
This loop outputs values i*j corresponding to the product of the current row value i, and each of the column values in turn by varying j from 1 to size. So for each iteration of the outer loop, the inner loop executes size iterations. The values are positioned in the same way as the column headings. When the outer loop is completed, the return is executed to end the program.
C++/CLI Programming Everything I have discussed in this chapter applies equally well in a C++/CLI program. Just to illustrate the point we can look at some examples of CLR console programs that demonstrate some of what you have learned so far in this chapter. The following is a CLR program that’s a slight variation on Ex3_01.
Try It Out
A CLR Program Using Nested if Statements
Create a CLR console program with the default code and modify the main() function like this: // Ex3_15.cpp : main project file. #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { wchar_t letter; // Corresponds to the C++/CLI Char type Console::Write(L”Enter a letter: “); letter = Console::Read(); if(letter >= ‘A’) // Test for ‘A’ or larger if(letter <= ‘Z’) // Test for ‘Z’ or smaller { Console::WriteLine(L”You entered a capital letter.”); return 0; } if(letter >= ‘a’) if(letter <= ‘z’)
150
// Test for ‘a’ or larger // Test for ‘z’ or smaller
Decisions and Loops { Console::WriteLine(L”You entered a small letter.”); return 0; } Console::WriteLine(L”You did not enter a letter.”); return 0; }
As always, the shaded lines are the new code you should add.
How It Works The logic is exactly the same as in the Ex3_01 example(in fact all the statements are the same except for those producing the output and the declaration of letter. I changed the type to wchar_t because type Char has some extra facilities that I’ll mention. The Console::Read() function reads a single character from the keyboard. Because you use the Console::Write() function to output the initial prompt there is no newline character issued so you can enter the letter on the same line as the prompt. The .NET Framework provides its own functions for converting character codes to upper- or lowercase within the Char class. These are the functions Char::ToUpper() and Char::ToLower() and you put the character to be converted between the parentheses as the argument to the function. For example: wchar_t uppcaseLetter = Char::ToUpper(letter);
Of course, you could store the result of the conversion back in the original variable, similar to the following. letter = Char::ToUpper(letter);
The Char class also provides IsUpper() and IsLower() functions that test whether a letter is upper- or lowercase. You pass the letter to be tested as the argument to the function and the function returns a bool value as a result. You could use these to code the main() function quite differently. wchar_t letter; // Corresponds to the C++/CLI Char type Console::Write(L”Enter a letter: “); letter = Console::Read(); wchar_t upper = Char::ToUpper(letter); if(upper >= ‘A’ && upper <= ‘Z’) // Test for between ‘A’ and ‘Z’ Console::WriteLine(L”You entered a {0} letter.”, Char::IsUpper(letter) ? “capital” : “small”); else Console::WriteLine(L”You did not enter a letter.”); return 0;
This simplifies the code considerably. After converting a letter to uppercase, you test the uppercase value to check whether it’s ‘A’ to ‘Z’. If it is, you output a message that depends on the result of the conditional operator expression that forms the second argument to the WriteLine() function. The conditional operator expression evaluates to “capital” if letter is uppercase and “small” if it is not, and this result is inserted in the output as determined by the position of the format string {0}.
151
Chapter 3 Try another CLR example that uses the Console::ReadKey() function in a loop and explores the ConsoleKeyInfo class a little more.
Try It Out
Reading Key Presses
Create a CLR console program and add the following code to main(). // Ex3_16.cpp : main project file. // Testing key presses in a loop. #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { Console::WriteLine(L”Press a key combination - press Escape to quit.”); ConsoleKeyInfo keyPress; do { keyPress = Console::ReadKey(true); Console::Write(L”You pressed”); if(safe_cast(keyPress.Modifiers)>0) Console::Write(L” {0},”, keyPress.Modifiers); Console::WriteLine(L” {0} which is the {1} character”, keyPress.Key, keyPress.KeyChar);
}while(keyPress.Key != ConsoleKey::Escape); return 0; }
The following is some sample output from this program. Press a key You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed You pressed
combination - press Escape to quit. Shift, B which is the B character Shift, Control, N which is the _ character Shift, Control, Oem1 which is the character Oem1 which is the ; character Oem3 which is the ‘ character Shift, Oem3 which is the @ character Shift, Oem7 which is the ~ character Shift, Oem6 which is the } character D3 which is the 3 character Shift, D3 which is the ? character Shift, D5 which is the % character Oem8 which is the ` character Escape which is the _ character
Of course, there are key combinations that do not represent a displayable character so in these cases there is no character in the output. The program also ends if you press Ctrl+C because the operating system recognizes this as the command to end the program.
152
Decisions and Loops How It Works Key presses are tested in the do-while loop and this loop continues until the Escape key is pressed. Within the loop the Console::ReadKey() function is called and the result is stored in the variable keyPress, which is of type ConsoleKeyInfo. The ConsoleKeyInfo class has three properties that you can access to help identify the key or keys that were pressed — the Key property identifies the key that was pressed, the KeyChar property represents the Unicode character code for the key, and the Modifiers property is a bitwise combination of ConsoleModifiers constants that represent the Shift, Alt, and Ctrl keys. ConsoleModifiers is an enumeration that is defined in the System library and the constants defined in the enumeration have the names Alt, Shift, and Control. As you can see from the arguments to the WriteLine() function in the last output statement, to access a property for an object you place the property name following the object name, separated by a period; the period is referred to as the member access operator. To access the KeyChar property for the object keyPress, you write keyPress.KeyChar. The operation of the program is very simple. Within the loop you call the ReadKey() function to read a key press and the result is stored in the variable keyPress. Next you write the initial part of the output to the command line using the Write() function; because no newline is written in this case, the next output statement writes to the same line. You then test whether the Modifiers property is greater that zero. If it is, modifier keys were pressed and you output them; otherwise, you skip the output for modifier keys. You will probably remember that a C++/CLI enumeration constants is an object that you must explicitly cast to an integer type before you can use it as a numerical value-hence the cast to type int in the if expression. The output of the Modifiers value is interesting. As you can see from the output, when more that one modifier key was pressed, you get all the modifier keys from the single output statement. This is because the Modifiers enumeration is defined with a FlagsAttribute attribute that indicates that this enumeration type is a set of single bit flags. This allows a variable of the enumeration type to consist of several flags ANDed together, and the individual flags recognize and output by the Write() or WriteLine() functions. The loop continues as long as the condition keyPress.Key != ConsoleKey::Escape is true. It is false when the keyPress.Key property is equal to ConsoleKey::Escape, which is when the Escape key is pressed.
The for each Loop All the loop statements I have discussed apply equally well to C++/CLI programs and the C++/CLI language provides you with the luxury of an additional kind of loop called the for each loop. This loop is specifically for iterating through all the objects in a particular kind of set of objects and because you haven’t learned about these yet, I’ll just introduce the for each loop briefly here, and elaborate on it some more a bit later in the book. One thing that you do know a little about is a String object, which represents a set of characters so you can use a for each loop to iterate through all the characters in a string. Let’s try an example of that.
153
Chapter 3 Try It Out
Using a for each Loop to Access the Characters in a String
Create a new CLR console program project with the name Ex3_17 and modify the code to the following: // Ex3_17.cpp : main project file. // Analyzing a string using a for each loop #include “stdafx.h” using namespace System;
int main(array<System::String ^> ^args) { int vowels = 0; int consonants = 0; String^ proverb = L”A nod is as good as a wink to a blind horse.”; for each(wchar_t ch in proverb) { if(Char::IsLetter(ch)) { ch = Char::ToLower(ch); // Convert to lowercase switch(ch) { case ‘a’: case ‘e’: case ‘i’: case ‘o’: case ‘u’: ++vowels; break; default: ++consonants; break; } } } Console::WriteLine(proverb); Console::WriteLine(L”The proverb contains {0} vowels and {1} consonants.”, vowels, consonants); return 0; }
This example produces the following output. A nod is as good as a wink to a blind horse. The proverb contains 14 vowels and 18 consonants.
How It Works The program counts the number of vowels and consonants in the string referenced by the proverb variable. The program does this by iterating over each character in the string using a for each loop. You first define two variables used to accumulate the total number of vowels and the total number of consonants. int vowels = 0; int consonants = 0;
154
Decisions and Loops Under the covers both are of the C++/CLI Int32 type, which stores a 32-bit integer. Next you define the string that to analyze. String^ proverb = L”A nod is as good as a wink to a blind horse.”;
The proverb variable is of type String^ that is described as type “handle to String”; a handle is used to store the location of an object on the garbage-collected heap that is managed by the CLR. You’ll learn more about handles and type String^ when we get into C++/CLI class types; for now, just take it that this is the type you use for C++/CLI variables that store strings. The for each loop that iterates over the characters in the string referenced by proverb is of this form. for each(wchar_t ch in proverb) { // Process the current character stored in ch... }
The characters in the proverb string are Unicode characters so you use a variable of type wchar_t (equivalent to type Char) to store them. The loop successively stores characters from the proverb string in the loop variable ch, which is of the C++/CLI type Char. This variable is local to the loop(in other words, it exists only within the loop block. On the first iteration ch contains the first character from the string, on the second iteration it contains the second character, on the third iteration the third character, and so on until all the characters have been processed and the loop ends. Within the loop you determine whether the character is a letter in the if expression: if(Char::IsLetter(ch))
The Char::IsLetter() function returns the value true if the argument(ch in this case(is a letter, and false otherwise. Thus the block following the if only executes if ch contains a letter. This is necessary because you don’t want punctuation characters to be processed as though they were letters. Having established that ch is indeed a letter you convert it to lowercase with the following statement. ch = Char::ToLower(ch);
// Convert to lowercase
This uses the Char::ToLower() function from the .NET Framework library which returns the lowercase equivalent of the argument(ch in this case. If the argument is already lowercase, the function just returns the same character code. By converting the character to lowercase, you avoid having to test subsequently for both upper- and lowercase vowels. You determine whether ch contains a vowel or a consonant within the switch statement. switch(ch) { case ‘a’: case ‘e’: case ‘i’: case ‘o’: case ‘u’: ++vowels; break; default:
155
Chapter 3 ++consonants; break; }
For any of the five cases where ch is a vowel, you increment the value stored in vowels; otherwise, you increment the value stored in consonants. This switch is executed for each character in proverb so when the loop finishes, vowels contain the number of vowels in the string and consonants contain the number of consonants. You then output the result with the following statements. Console::WriteLine(proverb); Console::WriteLine(L”The proverb contains {0} vowels and {1} consonants.”, vowels, consonants);
In the last statement, the value of vowels replaces the “{0}” in the string and the value of consonants replaces the “{1}”. This is because the arguments that follow the first format string argument are referenced by index values starting from 0.
Summar y In this chapter, you learned all of the essential mechanisms for making decisions in C++ programs. You have also gone through all the facilities for repeating a group of statements. The essentials of what I’ve discussed are as follows:
156
❑
The basic decision-making capability is based on the set of relational operators, which allow expressions to be tested and compared, and yield a bool value as the result(true or false.
❑
You can also make decisions based on conditions that return non-bool values. Any non-zero value are cast to true when a condition is tested; zero casts to false.
❑
The primary decision-making capability in C++ is provided by the if statement. Further flexibility is provided by the switch statement, and by the conditional operator.
❑
There are three basic methods provided in ISO/ANSI C++ for repeating a block of statements: the for loop, the while loop and the do-while loop. The for loop allows the loop to repeat a given number of times. The while loop allows a loop to continue as long as a specified condition returns true. Finally, do-while executes the loop at least once and allows continuation of the loop as long as a specified condition returns true.
❑
C++/CLI has the for each loop statement in addition to the three loop statements defined in ISO/ANSI C++.
❑
Any kind of loop may be nested within any other kind of loop.
❑
The keyword continue allows you to skip the remainder of the current iteration in a loop and go straight to the next iteration.
❑
The keyword break provides an immediate exit from a loop. It also provides an exit from a switch at the end of the statements for a case.
Decisions and Loops
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
Write a program that reads numbers from cin and then sums them, stopping when 0 has been entered. Construct three versions of this program, using the while, do-while and for loops.
2.
Write an ISO/ANSI C++ program to read characters from the keyboard and count the vowels. Stop counting when a Q (or a q) is encountered. Use a combination of an indefinite loop to get the characters, and a switch statement to count them.
3. 4.
Write a program to print out the multiplication tables from 2 to 12 in columns. Imagine that in a program you want to set a ‘file open mode’ variable based on two attributes: the file type which can be text or binary, and the way in which you want to open the file to read or write it or append data to it. Using the bitwise operators (& and |) and a set of flags, devise a method to allow a single integer variable to be set to any combination of the two attributes. Write a program which sets such a variable and then decodes it, printing out its setting, for all possible combinations of the attributes.
5.
Repeat Ex3_2 as a C++/CLI program(you can use Console::ReadKey() to read characters from the keyboard.
6.
Write a CLR console program that defines a string (as type String^) and then analyzes the characters in the string to discover the number of uppercase letters, the number of lowercase letters, the number of non-alphabetic characters and the total number of characters in the string.
157
4 Arrays, Strings, and Pointers So far, you have covered all the fundamental data types of consequence and you have a basic knowledge of how to perform calculations and make decisions in a program. This chapter is about broadening the application of the basic programming techniques that you have learned so far, from using single items of data to working with whole collections of data items. In this chapter, you will learn about: ❑
Arrays and how you use them
❑
How to declare and initialize arrays of different types
❑
How to declare and use multidimensional arrays
❑
Pointers is and how you use them
❑
How to declare and initialize pointers of different types
❑
The relationship between arrays and pointers
❑
References, how they are declared, and some initial ideas on their uses
❑
How to allocate memory for variables dynamically in a native C++ program
❑
How dynamic memory allocation works in a CLR program
❑
Tracking handles and tracking references and why you need them in a CLR program
❑
How to work with strings and arrays in C++/CLI programs
❑
What interior pointers are and how you can create and use them
In this chapter you’ll be using objects more extensively although you have not yet explored the details of how they are created so don’t worry if everything is not completely clear. You’ll learn about classes and objects in detail starting in Chapter 7.
Chapter 4
Handling Multiple Data Values of the Same Type You already know how to declare and initialize variables of various types that each holds a single item of information; I’ll refer to single items of data as data elements. You know how to create a single character in a variable of type char, a single integer in a variable of type short, type int, type long, or a single floating point number in a variable of type float or of type double. The most obvious extension to these ideas is to be able to reference several data elements of a particular type with a single variable name. This would enable you to handle applications of a much broader scope. Here’s an example of where you might need this. Suppose that you needed to write a payroll program. Using a separately named variable for each individual’s pay, their tax liability, and so on, would be an uphill task to say the least. A much more convenient way to handle such a problem would be to reference an employee by some kind of generic name — employeeName to take an imaginative example — and to have other generic names for the kinds of data related to each employee, such as pay, tax, and so on. Of course, you would also need some means of picking out a particular employee from the whole bunch, together with the data from the generic variables associated with them. This kind of requirement arises with any collection of like entities that you want to handle in your program, whether they’re baseball players or battleships. Naturally, C++ provides you with a way to deal with this.
Arrays The basis for the solution to all of these problems is provided by the array in ISO/ANSI C++. An array is simply a number of memory locations called array elements or simply elements, each of which can store an item of data of the same given data type and which are all referenced through the same variable name. The employee names in a payroll program could be stored in one array, the pay for each employee in another, and the tax due for each employee could be stored in a third array. Individual items in an array are specified by an index value which is simply an integer representing the sequence number of the elements in the array, the first having the sequence number 0, the second 1, and so on. You can also envisage the index value of an array element as being an offset from the first element in an array. The first element has an offset of 0 and therefore an index of 0, and an index value of 3 will refer to the fourth element of an array. For the payroll, you could arrange the arrays so that if an employee’s name was stored in the employeeName array at a given index value, then the arrays pay and tax would store the associated data on pay and tax for the same employee in the array positions referenced by the same index value. The basic structure of an array is illustrated in Figure 4-1. Figure 4-1 shows an array. The name height has six elements, each storing a different value. These might be the heights of the members of a family, for instance, recorded to the nearest inch. Because there are six elements, the index values run from 0 through 5. To refer to a particular element, you write the array name, followed by the index value of the particular element between square brackets. The third element is referred to as height[2], for example. If you think of the index as being the offset from the first element, it’s easy to see that the index value for the fourth element will be 3, for example. The amount of memory required to store each element is determined by its type, and all the elements of an array are stored in a contiguous block of memory.
160
Arrays, Strings, and Pointers Index value for the 2nd element
Index value for the 5th element
Array name
Array name
height[0]
height[1]
height[2]
height[3]
height[4]
height[5]
73
62
51
42
41
34
The height array has 6 elements. Figure 4-1
Declaring Arrays You declare an array in essentially the same way as you declared the variables that you have seen up to now, the only difference being that the number of elements in the array is specified between square brackets immediately following the array name. For example, you could declare the integer array height, shown in the previous figure, with the following declaration statement: long height[6];
Because each long value occupies 4 bytes in memory, the whole array requires 24 bytes. Arrays can be of any size, subject to the constraints imposed by the amount of memory in the computer on which your program is running. You can declare arrays to be of any type. For example, to declare arrays intended to store the capacity and power output of a series of engines, you could write the following: double cubic_inches[10]; double horsepower[10];
// Engine size // Engine power output
If auto mechanics are your thing, this would enable you to store the cubic capacity and power output of up to 10 engines, referenced by index values from 0 to 9. As you have seen before with other variables, you can declare multiple arrays of a given type in a single statement, but in practice it is almost always better to declare variables in separate statements.
Try It Out
Using Arrays
As a basis for an exercise in using arrays, imagine that you have kept a record of both the amount of gasoline you have bought for the car and the odometer reading on each occasion. You can write a program to analyze this data to see how the gas consumption looks on each occasion that you bought gas: // Ex4_01.cpp // Calculating gas mileage #include
161
Chapter 4 #include using using using using
std::cin; std::cout; std::endl; std::setw;
int main() { const int MAX = 20; double gas[ MAX ]; long miles[ MAX ]; int count = 0; char indicator = ‘y’;
// // // // //
Maximum number of values Gas quantity in gallons Odometer readings Loop counter Input indicator
while( (indicator == ‘y’ || indicator == ‘Y’) && count < MAX ) { cout << endl << “Enter gas quantity: “; cin >> gas[count]; // Read gas quantity cout << “Enter odometer reading: “; cin >> miles[count]; // Read odometer value ++count; cout << “Do you want to enter another(y or n)? “; cin >> indicator; } if(count <= 1) // count = 1 after 1 entry completed { // ... we need at least 2 cout << endl << “Sorry - at least two readings are necessary.”; return 0; } // Output results from 2nd entry to last entry for(int i = 1; i < count; i++) cout << endl << setw(2) << i << “.” // Output sequence number << “Gas purchased = “ << gas[i] << “ gallons” // Output gas << “ resulted in “ // Output miles per gallon << (miles[i] - miles[i - 1])/gas[i] << “ miles per gallon.”; cout << endl; return 0; }
The program assumes that you fill the tank each time so the gas bought was the amount consumed by driving the distance recorded. Here’s an example of the output produced by this example: Enter gas quantity: 12.8 Enter odometer reading: 25832 Do you want to enter another(y or n)? y Enter gas quantity: 14.9
162
Arrays, Strings, and Pointers Enter odometer reading: 26337 Do you want to enter another(y or n)? y Enter gas quantity: 11.8 Enter odometer reading: 26598 Do you want to enter another(y or n)? n 1.Gas purchased = 14.9 gallons resulted in 33.8926 miles per gallon. 2.Gas purchased = 11.8 gallons resulted in 22.1186 miles per gallon.
How It Works Because you need to take the difference between two odometer readings to calculate the miles covered for the gas used, you use the odometer reading only from the first pair of input values — you ignore the gas bought in the first instance as that would have been consumed during miles driven earlier. During the second period shown in the output, the traffic must have been really bad — or maybe the parking brake was always on. The dimensions of the two arrays gas and miles used to store the input data are determined by the value of the constant with the name MAX. By changing the value of MAX, you can change the program to accommodate a different maximum number of input values. This technique is commonly used to make a program flexible in the amount of information that it can handle. Of course, all the program code must be written to take account of the array dimensions, or of any other parameters being specified by const variables. This presents little difficulty in practice, however, so there’s no reason why you should not adopt this approach. You’ll also see later how to allocate memory for storing data as the program executes, so that you don’t need to fix the amount of memory allocated for data storage in advance.
Entering the Data The data values are read in the while loop. Because the loop variable count can run from 0 to MAX - 1, we haven’t allowed the user of our program to enter more values than the array can handle. You initialize the variables count and indicator to 0 and ‘y’ respectively, so that the while loop is entered at least once. There’s a prompt for each input value required and the value is read into the appropriate array element. The element used to store a particular value is determined by the variable count, which is 0 for the first input. The array element is specified in the cin statement by using count as an index, and count is then incremented ready for the next value. After you enter each value, the program prompts for confirmation that another value is to be entered. The character entered is read into the variable indicator and then tested in the loop condition. The loop will terminate unless ‘y’ or ‘Y’ is entered and the variable count is less than the specified maximum value, MAX. After the input loop ends (by whatever means), the value of count contains one more than the index value of the last element entered in each array. (Remember, you increment it after you enter each new element.) This is checked in order to verify that at least two pairs of values were entered. If this wasn’t the case, the program ends with a suitable message because two odometer values are necessary to calculate a mileage value.
163
Chapter 4 Producing the Results The output is generated in the for loop. The control variable i runs from 1 to count-1, allowing mileage to be calculated as the difference between the current element, miles[i] and the previous element, miles[i - 1]. Note that an index value can be any expression evaluating to an integer that represents a legal index for the array in question, which is an index value from 0 to one less than the number of elements in the array. If the value of an index expression lies outside of the range corresponding to legitimate array elements, you will reference a spurious data location that may contain other data, garbage, or even program code. If the reference to such an element appears in an expression, you will use some arbitrary data value in the calculation, which certainly produces a result that you did not intend. If you are storing a result in an array element using an illegal index value, you will overwrite whatever happens to be in that location. When this is part of your program code, the results are catastrophic. If you use illegal index values, there are no warnings produced either by the compiler or at run-time. The only way to guard against this is to code your program to prevent it happening. The output is generated by a single cout statement for all values entered, except for the first. A line number is also generated for each line of output using the loop control variable i. The miles per gallon are calculated directly in the output statement. You can use array elements in exactly the same way as any other variables in an expression.
Initializing Arrays To initialize an array in its declaration, you put the initializing values separated by commas between braces, and you place the set of initial values following an equals sign after the array name. Here’s an example of how you can declare and initialize an array: int cubic_inches[5] = { 200, 250, 300, 350, 400 };
The array has the name cubic_inches and has five elements that each store a value of type int. The values in the initializing list between the braces correspond to successive index values of the array, so in this case cubic_inches[0] has the value 200, cubic_inches[1] the value 250, cubic_inches[2] the value 300, and so on. You must not specify more initializing values than there are elements in the array, but you can include fewer. If there are fewer, the values are assigned to successive elements, starting with the first element — which is the one corresponding to the index value 0. The array elements for which you didn’t provide an initial value is initialized with zero. This isn’t the same as supplying no initializing list. Without an initializing list, the array elements contain junk values. Also, if you include an initializing list, there must be at least one initializing value in it; otherwise the compiler generates an error message. I can illustrate this with the following rather limited example.
Try It Out
Initializing an Array
// Ex4_02.cpp // Demonstrating array initialization #include #include using std::cout;
164
Arrays, Strings, and Pointers using std::endl; using std::setw; int main() { int value[5] = { 1, 2, 3 }; int junk [5]; cout << endl; for(int i = 0; i < 5; i++) cout << setw(12) << value[i]; cout << endl; for(int i = 0; i < 5; i++) cout << setw(12) << junk[i]; cout << endl; return 0; }
In this example, you declare two arrays, the first of which, value, you initialize in part, and the second, junk, you don’t initialize at all. The program generates two lines of output, which on my computer look like this: 1 -858993460
2 -858993460
3 -858993460
0 -858993460
0 -858993460
The second line (corresponding to values of junk[0] to junk[4]) may well be different on your computer.
How It Works The first three values of the array value are the initializing values and the last two have the default value of 0. In the case of junk, all the values are spurious because you didn’t provide any initial values at all. The array elements contain whatever values were left there by the program that last used these memory locations. A convenient way to initialize a whole array to zero is simply to specify a single initializing value as 0. For example: long data[100] = {0};
// Initialize all elements to zero
This statement declares the array data, with all one hundred elements initialized with 0. The first element is initialized by the value you have between the braces and the remaining elements are initialized to zero because you omitted values for these. You can also omit the dimension of an array of numeric type, providing you supply initializing values. The number of elements in the array is determined by the number of initializing values you specify. For example, the array declaration int value[] = { 2, 3, 4 };
defines an array with three elements that have the initial values 2, 3, and 4.
165
Chapter 4
Character Arrays and String Handling An array of type char is called a character array and is generally used to store a character string. A character string is a sequence of characters with a special character appended to indicate the end of the string. The string terminating character indicates the end of the string and this character is defined by the escape sequence ‘\0’, and is sometimes referred to as a null character, being a byte with all bits as zero. A string of this form is often referred to as a C-style string because defining a string in this way was introduced in the C language from which C++ was developed by Bjarne Stroustrup. This is not the only representation of a string that you can use — you’ll meet others later in the book. In particular, C++/CLI programs use a different representation of a string and the MFC defines a CString class to represent strings. The representation of a C-style string in memory is shown in Figure 4-2. name[4]
Each character in a string occupies one byte
String termination character
A l b e r
t
E i n s t e i n \0
char name[] = “Albert Einstein”;
Figure 4-2
Figure 4-2 illustrates how a string looks in memory and shows a form of declaration for a string that I’ll get to in a moment. Each character in the string occupies one byte, so together with the terminating null character, a string requires a number of bytes that is one greater than the number of characters contained in the string. You can declare a character array and initialize it with a string literal. For example: char movie_star[15] = “Marilyn Monroe”;
Note that the terminating ‘\0’is supplied automatically by the compiler. If you include one explicitly in the string literal, you end up with two of them. You must, however, allow for the terminating null in the number of elements that you allot to the array. You can let the compiler work out the length of an initialized array for you, as you saw in Figure 4-1. Here’s another example: char president[] = “Ulysses Grant”;
Because the dimension is unspecified, the compiler allocates space for enough elements to hold the initializing string, plus the terminating null character. In this case it allocates 14 elements for the array president. Of course, if you want to use this array later for storing a different string, its length (including the terminating null character) must not exceed 14 bytes. In general, it is your responsibility to ensure that the array is large enough for any string you might subsequently want to store.
166
Arrays, Strings, and Pointers String Input The header file contains definitions of a number of functions for reading characters from the keyboard. The one that you’ll look at here is the function getline(), which reads a sequence of characters entered through the keyboard and stores it in a character array as a string terminated by \0. You typically use the getline() function statements like this: const int MAX = 80; char name[MAX]; cin.getline(name, MAX, ‘\n’);
// Maximum string length including \0 // Array to store a string // Read input line as a string
These statements first declare a char array name with MAX elements and then read characters from cin using the function getline(). The source of the data, cin, is written as shown, with a period separating it from the function name. The significance of the arguments to the getline() function is shown in Figure 4-3. The maximum number of characters to be read. When the specified maximum has been read, input stops. The name of the array of type char[] in which the characters read from cin are to be stored.
The character that is to stop the input process. You can specify any character here, and the first occurance of that character will stop the input process.
cin.getline( name , MAX, ‘\n’ );
Figure 4-3
Because the last argument to the getline() function is ‘\n’(newline or end line character) and the second argument is MAX, characters are read from cin until the ‘\n’ character is read, or when MAX - 1 characters have been read, whichever occurs first. The maximum number of characters read is MAX - 1 rather than MAX to allow for the ‘\0’ character to be appended to the sequence of characters stored in the array. The ‘\n’ character is generated when you press the Return key on your keyboard and is therefore usually the most convenient character to end input. You can, however, specify something else by changing the last argument. The ‘\n’ isn’t stored in the input array name, but as I said, a ‘\0’ is added at the end of the input string in the array. You will learn more about this form of syntax when we discuss classes later on. Meanwhile, we’ll just take it for granted and use it in an example.
Try It Out
Programming With Strings
You now have enough knowledge to write a simple program to read a string and then count how many characters it contains. // Ex4_03.cpp // Counting string characters #include using std::cin;
167
Chapter 4 using std::cout; using std::endl; int main() { const int MAX = 80; char buffer[MAX]; int count = 0;
// Maximum array dimension // Input buffer // Character count
cout << “Enter a string of less than 80 characters:\n”; cin.getline(buffer, MAX, ‘\n’); // Read a string until \n while(buffer[count] != ‘\0’) count++;
// Increment count as long as // the current character is not null
cout << endl << “The string \”” << buffer << “\” has “ << count << “ characters.”; cout << endl; return 0; }
Typical output from this program is as follows: Enter a string of less than 80 characters: Radiation fades your genes The string “Radiation fades your genes” has 26 characters.
How It Works This program declares a character array buffer and reads a character string into the array from the keyboard after displaying a prompt for the input. Reading from the keyboard ends when the user presses Return, or when MAX-1 characters have been read. A while loop is used to count the number of characters read. The loop continues as long as the current character referenced with buffer[count] is not ‘\0’. This sort of checking on the current character while stepping through an array is a common technique in native C++. The only action in the loop is to increment count for each non-null character. There is also a library function, strlen(), that can save you the trouble of coding it yourself. If you use it, you need to include the header file in your program with an #include directive like this: #include
The ‘c’ in the header file name indicates that this file contains definitions that belong to the C language library that forms part of the C++ standard library. This header also contains the wcsnlen() function that will return the length of a wide character string. By using the strlen() function you could replace the while loop with the following statement: count = std::strlen(buffer);
168
Arrays, Strings, and Pointers The argument is the name of the array containing the string and the strlen() function returns the length of the string as an unsigned integer of type size_t. Many standard library functions return a value of type size_t and the size_t type is defined within the standard library using a typedef statement to be equivalent to one of the fundamental types, usually unsigned int. The reason for using size_t rather than a fundamental type directly is that it allows flexibility in what the actual type is in different C++ implementations. The C++ standard permits the range of values accommodated by a fundamental type to vary to make the best of a given hardware architecture and size_t can be defined to be the equivalent the most suitable fundamental type in the current machine environment. Finally in the example, the string and the character count is displayed with a single output statement. Note the use of the escape character ‘\”’ to output a double quote.
Multidimensional Arrays The arrays that you have defined so far with one index are referred to as one-dimensional arrays. An array can also have more than one index value, in which case it is called a multidimensional array. Suppose you have a field in which you are growing bean plants in rows of 10, and the field contains 12 such rows (so there are 120 plants in all). You could declare an array to record the weight of beans produced by each plant using the following statement: double beans[12][10];
This declares the two-dimensional array beans, the first index being the row number, and the second index the number within the row. To refer to any particular element requires two indices. For example, you could set the value of the element reflecting the fifth plant in the third row with the following statement: beans[2][4] = 10.7;
Remember that the index values start from zero, so the row index value is 2 and the index for the fifth plant within the row is 4. Being a successful bean farmer, you might have several identical fields planted with beans in the same pattern. Assuming that you have eight fields, you could use a three-dimensional array to record data about these, declared thus: double beans[8][12][10];
This records production for all of the plants in each of the fields, the leftmost index referencing a particular field. If you ever get to bean farming on an international scale, you are able to use a four-dimensional array, with the extra dimension designating the country. Assuming that you’re as good a salesman as you are a farmer, growing this quantity of beans to keep up with the demand may well start to affect the ozone layer. Arrays are stored in memory such that the rightmost index value varies most rapidly. Thus the array data[3][4] is three one-dimensional arrays of four elements each. The arrangement of this array is illustrated in Figure 4-4. The elements of the array are stored in a contiguous block of memory, as indicated by the arrows in Figure 4-4. The first index selects a particular row within the array and the second index selects an element within the row.
169
Chapter 4 data[0][0]
data[0][1]
data[0][2]
data[0][3]
data[1][0]
data[1][1]
data[1][2]
data[1][3]
data[2][0]
data[2][1]
data[2][2]
data[2][3]
The array elements are stored in contiguous locations in memory. Figure 4-4
Note that a two-dimensional array in native C++ is really a one-dimensional array of one-dimensional arrays. A native C++ array with three dimensions is actually a one-dimensional array of elements where each element is a one-dimensional array of on-dimensional arrays. This is not something you need to worry about most of the time, but as you will see later, C++/CLI arrays are not the same as this. It also implies that for the array in Figure 4-4 the expressions data[0], data[1], and data[2], represent onedimensional arrays.
Initializing Multidimensional Arrays To initialize a multidimensional array, you use an extension of the method used for a one-dimensional array. For example, you can initialize a two-dimensional array, data, with the following declaration: long data[2][4] = { { 1, 2, 3, 5 }, { 7, 11, 13, 17 } };
Thus, the initializing values for each row of the array are contained within their own pair of braces. Because there are four elements in each row, there are four initializing values in each group, and because there are two rows, there are two groups between braces, each group of initializing values being separated from the next by a comma. You can omit initializing values in any row, in which case the remaining array elements in the row are zero. For example: long data[2][4] = { { 1, 2, { 7, 11 };
170
3
}, }
Arrays, Strings, and Pointers I have spaced out the initializing values to show where values have been omitted. The elements data[0][3], data[1][2], and data[1][3] have no initializing values and are therefore zero. If you wanted to initialize the whole array with zeros you could simply write: long data[2][4] = {0};
If you are initializing arrays with even more dimensions, remember that you need as many nested braces for groups of initializing values as there are dimensions in the array.
Try It Out
Storing Multiple Strings
You can use a single two-dimensional array to store several C-style strings. You can see how this works with an example: // Ex4_04.cpp // Storing strings in an array. #include using std::cout; using std::cin; using std::endl; int main() { char stars[6][80] = { “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” }; int dice = 0; cout << endl << “ Pick a lucky star!” << “ Enter a number between 1 and 6: “; cin >> dice; if(dice >= 1 && dice <= 6) // Check input validity cout << endl // Output star name << “Your lucky star is “ << stars[dice - 1]; else cout << endl // Invalid input << “Sorry, you haven’t got a lucky star.”; cout << endl; return 0; }
How It Works Apart from its incredible inherent entertainment value, the main point of interest in this example is the declaration of the array stars. It is a two-dimensional array of elements of type char that can hold up
171
Chapter 4 to six strings, each of which can be up to 80 characters long (including the terminating null character that is automatically added by the compiler). The initializing strings for the array are enclosed between braces and separated by commas. One disadvantage of using arrays in this way is the memory that is almost invariably left unused. All of the strings are fewer than 80 characters and the surplus elements in each row of the array are wasted. You can also let the compiler work out how many strings you have by omitting the first array dimension and declaring it as follows: char stars[][80] = { “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” };
This causes the compiler to define the first dimension to accommodate the number of initializing strings that you have specified. Because you have six, the result is exactly the same, but it avoids the possibility of an error. Here you can’t omit both array dimensions. With an array of two or more dimensions the rightmost dimension must always be defined. Note the semicolon at the end of the declaration. It’s easy to forget it when there are initializing values for an array. Where you need to reference a string for output in the following statement, you need only specify the first index value: cout << endl // Output star name << “Your lucky star is “ << stars[dice - 1];
A single index value selects a particular 80-element sub-array, and the output operation displays the contents up to the terminating null character. The index is specified as dice - 1 as the dice values are from 1 to 6, whereas the index values clearly need to be from 0 to 5.
Indirect Data Access The variables that you have dealt with so far provide you with the ability to name a memory location in which you can store data of a particular type. The contents of a variable are either entered from an external source, such as the keyboard, or calculated from other values that are entered. There is another kind of variable in C++ that does not store data that you normally enter or calculate, but greatly extends the power and flexibility of your programs. This kind of variable is called a pointer.
What Is a Pointer? Each memory location that you use to store a data value has an address. The address provides the means for your PC hardware to reference a particular data item. A pointer is a variable that stores an address of
172
Arrays, Strings, and Pointers another variable of a particular type. A pointer has a variable name just like any other variable and also has a type that designates what kind of variables its contents refer to. Note that the type of a pointer variable includes the fact that it’s a pointer. A variable that is a pointer that can contain addresses of locations in memory containing values of type int, is of type ‘pointer to int’.
Declaring Pointers The declaration for a pointer is similar to that of an ordinary variable, except that the pointer name has an asterisk in front of it to indicate that it’s a variable that is a pointer. For example, to declare a pointer pnumber of type long, you could use the following statement: long* pnumber;
This declaration has been written with the asterisk close to the type name. If you want, you can also write it as: long *pnumber;
The compiler won’t mind at all; however, the type of the variable pnumber is ‘pointer to long’, which is often indicated by placing the asterisk close to the type name. Whichever way you choose to write a pointer type, be consistent. You can mix declarations of ordinary variables and pointers in the same statement. For example: long* pnumber, number = 99;
This declares the pointer pnumber of type ‘pointer to long’ as before, and also declares the variable number, of type long. On balance, it’s probably better to declare pointers separately from other variables; otherwise, the statement can appear misleading as to the type of the variables declared, particularly if you prefer to place the * adjacent to the type name. The following statements certainly look clearer and putting declarations on separate lines enables you to add comments for them individually, making for a program that is easier to read. long number = 99; long* pnumber;
// Declaration and initialization of long variable // Declaration of variable of type pointer to long
It’s a common convention in C++ to use variable names beginning with p to denote pointers. This makes it easier to see which variables in a program are pointers, which in turn can make a program easier to follow. Let’s take an example to see how this works, without worrying about what it’s for. I will get to how you use pointers very shortly. Suppose you have the long integer variable number because you declared it above containing the value 99. You also have the pointer, pnumber, of type pointer to long, which you could use to store the address of the variable number. But how do you obtain the address of a variable?
The Address-Of Operator What you need is the address-of operator, &. This is a unary operator that obtains the address of a variable. It’s also called the reference operator, for reasons I will discuss later in this chapter. To set up the pointer that I have just discussed, you could write this assignment statement: pnumber = &number;
// Store address of number in pnumber
173
Chapter 4 The result of this operation is illustrated in Figure 4-5. &number
Address: 1008
pnumber
number
1008
99 pnumber = &number;
Figure 4-5
You can use the operator & to obtain the address of any variable, but you need a pointer of the appropriate type to store it. If you want to store the address of a double variable for example, the pointer must have been declared as type double*, which is type ‘pointer to double’.
Using Pointers Taking the address of a variable and storing it in a pointer is all very well, but the really interesting aspect is how you can use it. Fundamental to using a pointer is accessing the data value in the variable to which a pointer points. This is done using the indirection operator, *.
The Indirection Operator You use the indirection operator, *, with a pointer to access the contents of the variable that it points to. The name ‘indirection operator’ stems from the fact that the data is accessed indirectly. It is also called the de-reference operator, and the process of accessing the data in the variable pointed to by a pointer is termed de-referencing the pointer. One aspect of this operator that can seem confusing is the fact that you now have several different uses for the same symbol, *. It is the multiply operator, it also serves as the indirection operator, and it is used in the declaration of a pointer. Each time you use *, the compiler is able to distinguish its meaning by the context. When you multiply two variables, A*B for instance, there’s no meaningful interpretation of this expression for anything other than a multiply operation.
Why Use Pointers? A question that usually springs to mind at this point is, “Why use pointers at all?” After all, taking the address of a variable you already know and sticking it in a pointer so that you can de-reference it seems like an overhead you can do without. There are several reasons why pointers are important. As you will see shortly, you can use pointer notation to operate on data stored in an array, which often executes faster than if you use array notation. Also, when you get to define your own functions later in the book, you will see that pointers are used extensively for enabling access within a function to large
174
Arrays, Strings, and Pointers blocks of data, such as arrays, that are defined outside the function. Most importantly, however, you will also see later that you can allocate space for variables dynamically, that is, during program execution. This sort of capability allows your program to adjust its use of memory depending on the input to the program. Because you don’t know in advance how many variables you are going to create dynamically, a primary way you have for doing this is by using pointers — so make sure you get the hang of this bit.
Try It Out
Using Pointers
You can try out various aspects of pointer operations with an example: //Ex4_05.cpp // Exercising pointers #include using std::cout; using std::endl; using std::hex; using std::dec; int main() { long* pnumber = NULL; // Pointer declaration & initialization long number1 = 55, number2 = 99; pnumber = &number1; // Store address in pointer *pnumber += 11; // Increment number1 by 11 cout << endl << “number1 = “ << number1 << “ &number1 = “ << hex << pnumber; pnumber = &number2; number1 = *pnumber*10; cout << << << <<
// Change pointer to address of number2 // 10 times number2
endl “number1 = “ << dec << number1 “ pnumber = “ << hex << pnumber “ *pnumber = “ << dec << *pnumber;
cout << endl; return 0; }
On my computer, this example generates the following output: number1 = 66 number1 = 990
&number1 = 0012FEC8 pnumber = 0012FEBC
*pnumber = 99
How It Works There is no input to this example. All operations are carried out with the initializing values for the variables. After storing the address of number1 in the pointer pnumber, the value of number1 is incremented indirectly through the pointer in this statement: *pnumber += 11;
// Increment number1 by 11
175
Chapter 4 Note that when you first declared the pointer pnumber, you initialized it to NULL. I’ll discuss pointer initialization in the next section. The indirection operator determines that you are adding 11 to the contents of the variable pointed to by pnumber, which is number1. If you forgot the * in this statement, you would be attempting to add 11 to the address stored in the pointer. The values of number1, and the address of number1 that is stored in pnumber, are displayed. You use the hex manipulator to generate the address output in hexadecimal notation. You can obtain the value of ordinary integer variables as hexadecimal output by using the manipulator hex. You send it to the output stream in the same way that you have applied endl, with the result that all following output is in hexadecimal notation. If you want the following output to be decimal, you need to use the manipulator dec in the next output statement to switch the output back to decimal mode again. After the first line of output, the contents of pnumber are set to the address of number2. The variable number1 is then changed to the value of 10 times number2: number1 = *pnumber*10;
// 10 times number2
This is calculated by accessing the contents of number2 indirectly through the pointer. The second line of output shows the results of these calculations The address values you see in your output may well be different from those shown in the output here since they reflect where the program is loaded in memory, which depends on how your operating system is configured. The 0x prefixing the address values indicates that they are hexadecimal numbers. Note that the addresses &number1 and pnumber (when it contains &number2) differ by four bytes. This shows that number1 and number2 occupy adjacent memory locations, as each variable of type long occupies four bytes. The output demonstrates that everything is working as you would expect.
Initializing Pointers Using pointers that aren’t initialized is extremely hazardous. You can easily overwrite random areas of memory through an uninitialized pointer. The resulting damage just depends on how unlucky you are, so it’s more than just a good idea to initialize your pointers. It’s very easy to initialize a pointer to the address of a variable that has already been defined. Here you can see that I have initialized the pointer pnumber with the address of the variable number just by using the operator & with the variable name: int number = 0; int* pnumber = &number;
// Initialized integer variable // Initialized pointer
When initializing a pointer with the address of another variable, remember that the variable must already have been declared prior to the pointer declaration. Of course, you may not want to initialize a pointer with the address of a specific variable when you declare it. In this case, you can initialize it with the pointer equivalent of zero. For this, Visual C++ provides the symbol NULL that is already defined as 0, so you can declare and initialize a pointer using the following statement, rather like you did in the last example: int* pnumber = NULL;
176
// Pointer not pointing to anything
Arrays, Strings, and Pointers This ensures that the pointer doesn’t contain an address that will be accepted as valid and provides the pointer with a value that you can check in an if statement, such as: if(pnumber == NULL) cout << endl << “pnumber is null.”;
Of course, you can also initialize a pointer explicitly with 0, which also ensures that it is assigned a value that doesn’t point to anything. No object can be allocated the address 0, so in effect 0 used as an address indicates that the pointer has no target. In spite of it being arguably somewhat less legible, if you expect to run your code with other compilers, it is preferable to use 0 as an initializing value for a pointer that you want to be null. This is also more consistent with the current ‘good practice’ in ISO/ANSI C++, the argument being that if you have an object with a name in C++, it should have a type; however, NULL does not have a type — it’s an alias for 0. As you’ll see later in this chapter, things are a little different in C++/CLI. To use 0 as the initializing value for a pointer you simply write: int* pnumber = 0;
// Pointer not pointing to anything
To check whether a pointer contains a valid address, you could use the statement: if(pnumber == 0) cout << endl << “pnumber is null.”;
Equally well, you could use the statement: if(!pnumber) cout << endl << “pnumber is null.”;
This statement does exactly the same as the previous example. Of course, you can also use the form: if(pnumber != 0) // Pointer is valid, so do something useful
The address pointed to by the NULL pointer contains a junk value. You should never attempt to de-reference a null pointer, because it will cause your program to end immediately.
Pointers to char A pointer of type char* has the interesting property that it can be initialized with a string literal. For example, we can declare and initialize such a pointer with the statement: char* proverb = “A miss is as good as a mile.”;
This looks similar to initializing a char array, but it’s slightly different. This creates a string literal (actually an array of type const char) with the character string appearing between the quotes and terminated with /0, and store the address of the literal in the pointer proverb. The address of the literal will be the address of its first character. This is shown in Figure 4-6.
177
Chapter 4 1. The pointer proverb is created. 3. The address of the string is stored in the pointer.
proverb 1000
Address: 1000
A
m i s s
i s
a s
g o o d
a s
a
m i
2. The constant string is created, terminated with \0. Figure 4-6
Try It Out
Lucky Stars With Pointers
You could rewrite the lucky stars example using pointers instead of an array to see how that would work: // Ex4_06.cpp // Initializing pointers with strings #include using std::cin; using std::cout; using std::endl; int main() { char* pstr1 char* pstr2 char* pstr3 char* pstr4 char* pstr5 char* pstr6 char* pstr
= = = = = = =
“Robert Redford”; “Hopalong Cassidy”; “Lassie”; “Slim Pickens”; “Boris Karloff”; “Oliver Hardy”; “Your lucky star is “;
int dice = 0; cout << endl << “ Pick a lucky star!” << “ Enter a number between 1 and 6: “; cin >> dice; cout << endl; switch(dice) { case 1: cout << pstr << pstr1; break; case 2: cout << pstr << pstr2;
178
l e . \0
Arrays, Strings, and Pointers break; case 3: cout << pstr << pstr3; break; case 4: cout << pstr << pstr4; break; case 5: cout << pstr << pstr5; break; case 6: cout << pstr << pstr6; break; default: cout << “Sorry, you haven’t got a lucky star.”; } cout << endl; return 0; }
How It Works The array in Ex4_04.cpp has been replaced by the six pointers, pstr1 to pstr6, each initialized with a name. You also have declared an additional pointer, pstr, initialized with the phrase that you want to use at the start of a normal output line. Because you have discrete pointers, it is easier to use a switch statement to select the appropriate output message than to use an if as you did in the original version. Any incorrect values entered are all taken care of by the default option of the switch. Outputting the string pointed to by a pointer couldn’t be easier. As you can see, you simply write the pointer name. It may cross your mind at this point that in Ex4_05.cpp you wrote a pointer name in the output statement and the address that it contained was displayed. Why is it different here? The answer lays in the way the output operation views a pointer of type ‘pointer to char’. It treats a pointer of this type in a special way in that it regards it as a string (which is an array of char), and so outputs the string itself, rather than its address. Using pointers in the example has eliminated the waste of memory that occurred with the array version of this program, but the program seems a little long-winded now — there must be a better way. Indeed there is — using an array of pointers.
Try It Out
Arrays of Pointers
With an array of pointers of type char, each element can point to an independent string, and the lengths of each of the strings can be different. You can declare an array of pointers in the same way that you declare a normal array. Let’s go straight to rewriting the previous example using a pointer array: // Ex4_07.cpp // Initializing pointers with strings #include using std::cin; using std::cout; using std::endl; int main()
179
Chapter 4 { char* pstr[] =
{ “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” }; char* pstart = “Your lucky star is “;
// Initializing a pointer array
int dice = 0; cout << endl << “ Pick a lucky star!” << “ Enter a number between 1 and 6: “; cin >> dice; cout << endl; if(dice >= 1 && dice <= 6) cout << pstart << pstr[dice - 1];
// Check input validity // Output star name
else cout << “Sorry, you haven’t got a lucky star.”; // Invalid input cout << endl; return 0; }
How It Works In this case, you are nearly getting the best of all possible worlds. You have a one-dimensional array of pointers to type char declared such that the compiler works out what the dimension should be from the number of initializing strings. The memory usage that results from this is illustrated in Figure 4-7. Compared to using a ‘normal’ array, the pointer array carries less overhead in terms of space. With an array, you would need to make each row the length of the longest string, and six rows of seventeen bytes each is 102 bytes, so by using a pointer array we have saved a whole -1 bytes! What’s gone wrong? The simple truth is that for this small number of relatively short strings, the size of the extra array of pointers is significant. You would make savings if you were dealing with more strings that were longer and had more variable lengths. Space saving isn’t the only advantage that you get by using pointers. In a lot of circumstances you save time too. Think of what happens if you want to move “Oliver Hardy” to the first position and “Robert Redford” to the end. With the pointer array as above you just need to swap the pointers — the strings themselves stay where they are. If you had stored these simply as strings, as you did in Ex4_04.cpp, a great deal of copying would be necessary — you need to copy the whole string “Robert Redford” to a temporary location while you copied “Oliver Hardy” in its place, and then you need to copy “Robert Redford” to the end position. This requires significantly more computer time to execute.
180
Arrays, Strings, and Pointers 15 bytes R o b e r
t
R e d f o r d \0
17 bytes H o p a l o n g pstr[0] pstr[1] pstr[2] pstr[3] pstr[4] pstr[5]
7 bytes L a s s i e \0
Pointer array 24 bytes
14 bytes B o r i s
13 bytes S l i m
P i
13 bytes O l i v e r
C a s s i d y \0
c k e n s \0
K a r
l o f
f \0
H a r d y \0
Total Memory is 103 bytes
Figure 4-7
Because you are using pstr as the name of the array, the variable holding the start of the output message needs to be different; it is called pstart. You select the string that you want to output by means of a very simple if statement, similar to that of the original version of the example. You either display a star selection or a suitable message if the user enters an invalid value. One weakness of the way the program is written is that the code assumes there are six options, even though the compiler is allocating the space for the pointer array from the number of initializing strings that you supply. So if you add a string to the list, you have to alter other parts of the program to take account of this. It would be nice to be able to add strings and have the program automatically adapt to however many strings there are.
The sizeof Operator A new operator can help us here. The sizeof operator produces an integer value of type size_t that gives the number of bytes occupied by its operand. You’ll recall from the earlier discussion that size_t is a type defined by the standard library and is usually the same as unsigned int. Look at this statement that refers to the variable dice from the previous example: cout << sizeof dice;
The value of the expression sizeof dice is 4 because dice was declared as type int and therefore occupies 4 bytes. Thus this statement outputs the value 4.
181
Chapter 4 The sizeof operator can be applied to an element in an array or to the whole array. When the operator is applied to an array name by itself, it produces the number of bytes occupied by the whole array, whereas when it is applied to a single element with the appropriate index value or values, it results in the number of bytes occupied by that element. Thus, in the last example, we could output the number of elements in the pstr array with the expression: cout << (sizeof pstr)/(sizeof pstr[0]);
The expression (sizeof pstr)/(sizeof pstr[0]) divides the number of bytes occupied by the whole pointer array by the number of bytes occupied by the first element of the array. Because each element in the array occupies the same amount of memory, the result is the number of elements in the array. Remember that pstr is an array of pointers — using the sizeof operator on the array or on individual elements will not tell us anything about the memory occupied by the text strings. You can also apply the sizeof operator to a type name rather than a variable, in which case the result is the number of bytes occupied by a variable of that type. In this case the type name should be enclosed between parentheses. For example, after executing the statement, size_t_size = sizeof(long);
the variable long_size will have the value 4. The variable size is declared to be of type size_t to match the type of the value produced by the sizeof operator. Using a different integer type for size may result in a warning message from the compiler.
Try It Out
Using the sizeof Operator
You can amend the last example to use the sizeof operator so that the code automatically adapts to an arbitrary number of string values from which to select: // Ex4_08.cpp // Flexible array management using sizeof #include using std::cin; using std::cout; using std::endl; int main() { char* pstr[] = { “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” }; char* pstart = “Your lucky star is “;
182
// Initializing a pointer array
Arrays, Strings, and Pointers int count = (sizeof pstr)/(sizeof pstr[0]);
// Number of array elements
int dice = 0; cout << endl << “ Pick a lucky star!” << “ Enter a number between 1 and “ << count << “: “; cin >> dice; cout << if(dice cout else cout
endl; >= 1 && dice <= count) << pstart << pstr[dice - 1];
// Check input validity // Output star name
<< “Sorry, you haven’t got a lucky star.”; // Invalid input
cout << endl; return 0; }
How It Works As you can see, the changes required in the example are very simple. We just calculate the number of elements in the pointer array pstr and store the result in count. Then, wherever the total number of elements in the array was referenced as 6, we just use the variable count. You could now just add a few more names to the list of lucky stars and everything affected in the program is adjusted automatically.
Constant Pointers and Pointers to Constants The array pstr in the last example is clearly not intended to be modified in the program, and nor are the strings being pointed to, nor the variable count. It would be a good idea to ensure that these didn’t get modified by mistake in the program. You could very easily protect the variable count from accidental modification by writing this: const int count = (sizeof pstr)/(sizeof pstr[0]);
However, the array of pointers deserves closer examination. You declared the array like this: char* pstr[] = { “Robert Redford”, // Initializing a pointer array “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” };
Each pointer in the array is initialized with the address of a string literal, “Robert Redford,” “Hopalong Cassidy,” and so on. The type of a string literal is ‘array of const char’ so you are storing the address of a const array in a non-const pointer. The reason the compiler allows us to use a string literal to initialize an element of an array of char* is for reasons of backwards compatibility with existing code.
183
Chapter 4 If you try to alter the character array with a statement like this: *pstr[0] = “Stan Laurel”;
the program does not compile. If you were to reset one of the elements of the array to point to a character using a statement like this: *pstr[0] = ‘X’;
the program compiles but crashes when this statement was executed. You don’t really want to have unexpected behavior like the program crashing at run time, and you can prevent it. A far better way of writing the declaration is as follows: const char* pstr[] = { “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” };
// Array of pointers // to constants
In this case, there is no ambiguity about the const-ness of the strings pointed to by the elements of the pointer array. If you now attempt to change these strings, the compiler flags this as an error at compile time. However, you could still legally write this statement: pstr[0] = pstr[1];
Those lucky individuals due to be awarded Mr. Redford would get Mr. Cassidy instead because both pointers now point to the same name. Note that this isn’t changing the values of the objects pointed to by the pointer array element — it is changing the value of the pointer stored in pstr[0]. You should therefore inhibit this kind of change as well because some people may reckon that good old Hoppy may not have the same sex appeal as Robert. You can do this with the following statement: // Array of constant pointers to constants const char* const pstr[] = { “Robert Redford”, “Hopalong Cassidy”, “Lassie”, “Slim Pickens”, “Boris Karloff”, “Oliver Hardy” };
To summarize, you can distinguish three situations relating to const, pointers and the objects to which they point:
184
❑
A pointer to a constant object
❑
A constant pointer to an object
❑
A constant pointer to a constant object
Arrays, Strings, and Pointers In the first situation, the object pointed to cannot be modified, but we can set the pointer to point to something else: const char* pstring = “Some text”;
In the second, the address stored in the pointer can’t be changed, but the object pointed to can be: char* const pstring = “Some text”;
Finally, in the third situation, both the pointer and the object pointed to have been defined as constant and, therefore, neither can be changed: const char* const pstring = “Some text”;
Of course, all this applies to pointers to any type. A pointer to type char is used here purely for illustrative purposes.
Pointers and Arrays Array names can behave like pointers under some circumstances. In most situations, if you use the name of a one-dimensional array by itself, it is automatically converted to a pointer to the first element of the array. Note that this is not the case when the array name is used as the operand of the sizeof operator. If we have these declarations, double* pdata; double data[5];
you can write this assignment: pdata = data;
// Initialize pointer with the array address
This is assigning the address of the first element of the array data to the pointer pdata. Using the array name by itself refers to the address of the array. If you use the array name data with an index value, it refers to the contents of the element corresponding to that index value. So, if you want to store the address of that element in the pointer, you have to use the address-of operator: pdata = &data[1];
Here, the pointer pdata contains the address of the second element of the array.
Pointer Arithmetic You can perform arithmetic operations with pointers. You are limited to addition and subtraction in terms of arithmetic, but you can also perform comparisons of pointer values to produce a logical result. Arithmetic with a pointer implicitly assumes that the pointer points to an array, and that the arithmetic operation is on the address contained in the pointer. For the pointer pdata for example, we could assign the address of the third element of the array data to a pointer with this statement: pdata = &data[2];
185
Chapter 4 In this case, the expression pdata+1 would refer to the address of data[3], the fourth element of the data array, so you could make the pointer point to this element by writing this statement: pdata += 1;
// Increment pdata to the next element
This statement increments the address contained in pdata by the number of bytes occupied by one element of the array data. In general, the expression pdata+n, where n can be any expression resulting in an integer, adds n*sizeof(double) to the address contained in the pointer pdata because it was declared to be of type pointer to double. This is illustrated in Figure 4-8.
double data[5];
data[0]
data[1]
Each element occupies 8 bytes
data[2]
data[3]
data[4]
Address
pdata+1
pdata+2
pdata = &data[2]; Figure 4-8
In other words, incrementing or decrementing a pointer works in terms of the type of the object pointed to. Increasing a pointer to long by one changes its contents to the next long address and so increments the address by four. Similarly, incrementing a pointer to short by one increments the address by two. The more common notation for incrementing a pointer is using the increment operator. For example: pdata++;
// Increment pdata to the next element
This is equivalent to (and more common than) the += form. However, I used the preceding += form to make it clear that although the increment value is actually specified as one, the effect is usually an increment greater than one, except in the case of a pointer to type char. The address resulting from an arithmetic operation on a pointer can be a value ranging from the address of the first element of the array to the address that is one beyond the last element. Outside of these limits, the behavior of the pointer is undefined. You can, of course, de-reference a pointer on which you have performed arithmetic (there wouldn’t be much point to it otherwise). For example, assuming that pdata is still pointing to data[2], this statement, *(pdata + 1) = *(pdata + 2);
186
Arrays, Strings, and Pointers is equivalent to this: data[3] = data[4];
When you want to de-reference a pointer after incrementing the address it contains, the parentheses are necessary because the precedence of the indirection operator is higher than that of the arithmetic operators, + or -. If you write the expression *pdata + 1, instead of *(pdata + 1), this adds one to the value stored at the address contained in pdata, which is equivalent to executing data[2] + 1. Because this isn’t an lvalue, its use in the previous assignment statement causes the compiler to generate an error message. You can use an array name as though it were a pointer for addressing elements of an array. If you have the same one-dimensional array as before, declared as long data[5];
using pointer notation, you can refer to the element data[3] for example as *(data + 3). This kind of notation can be applied generally so that, corresponding to the elements data[0], data[1], data[2], you can write *data, *(data + 1), *(data+2), and so on.
Try It Out
Array Names as Pointers
You could exercise this aspect of array addressing with a program to calculate prime numbers (a prime number is divisible only by itself and one). // Ex4_09.cpp // Calculating primes #include #include using std::cout; using std::endl; using std::setw; int main() { const int MAX = 100; long primes[MAX] = { 2,3,5 }; long trial = 5; int count = 3; int found = 0;
// // // // //
Number of primes required First three primes defined Candidate prime Count of primes found Indicates when a prime is found
do { trial += 2; // Next value for checking found = 0; // Set found indicator for(int i = 0; i < count; i++) // Try division by existing primes { found = (trial % *(primes + i)) == 0;// True for exact division if(found) // If division is exact break; // it’s not a prime } if (found == 0) // We got one...
187
Chapter 4 *(primes + count++) = trial; }while(count < MAX);
// ...so save it in primes array
// Output primes 5 to a line for(int i = 0; i < MAX; i++) { if(i % 5 == 0) // New line on 1st, and every 5th line cout << endl; cout << setw(10) << *(primes + i); } cout << endl; return 0; }
If you compile and execute this example, you should get the following output: 2 13 31 53 73 101 127 151 179 199 233 263 283 317 353 383 419 443 467 503
3 17 37 59 79 103 131 157 181 211 239 269 293 331 359 389 421 449 479 509
5 19 41 61 83 107 137 163 191 223 241 271 307 337 367 397 431 457 487 521
7 23 43 67 89 109 139 167 193 227 251 277 311 347 373 401 433 461 491 523
11 29 47 71 97 113 149 173 197 229 257 281 313 349 379 409 439 463 499 541
How It Works You have the usual #include statements for the header file for input and output and for because you will use a stream manipulator to set the field width for output. You use the constant MAX to define the number of primes that you want the program to produce. The primes array, which stores the results, has the first three primes already defined to start the process off. All the work is done in two loops: the outer do-while loop, which picks the next value to be checked and adds the value to the primes array if it is prime, and the inner for loop that actually checks the value to see whether it’s prime or not. The algorithm in the for loop is very simple and is based on the fact that if a number is not a prime, it must be divisible by one of the primes found so far — all of which are less than the number in question because all numbers are either prime or a product of primes. In fact, only division by primes less than or
188
Arrays, Strings, and Pointers equal to the square root of the number in question need to be checked, so this example isn’t as efficient as it might be. found = (trial % *(primes + i)) == 0;
// True for exact division
This statement sets the variable found to be 1 if there’s no remainder from dividing the value in trial by the current prime *(primes + i) (remember that this is equivalent to primes[i]), and 0 otherwise. The if statement causes the for loop to be terminated if found has the value 1 because the candidate in trial can’t be a prime in that case. After the for loop ends (for whatever reason), it’s necessary to decide whether or not the value in trial was prime. This is indicated by the value in the indicator variable found. *(primes + count++) = trial;
// ...so save it in primes array
If trial does contain a prime, this statement stores the value in primes[count] and then increments count through the postfix increment operator. After MAX number of primes have been found, they are output with a field width of 10 characters, 5 to a line, as a result of this statement: if(i % 5 == 0) cout << endl;
// New line on 1st, and every 5th line
This starts a new line when i has the values 0, 5, 10, and so on.
Try It Out
Counting Characters Revisited
To see how handling strings works in pointer notation, you could produce a version of the program you looked at earlier for counting the characters in a string: // Ex4_10.cpp // Counting string characters using a pointer #include using std::cin; using std::cout; using std::endl; int main() { const int MAX = 80; char buffer[MAX]; char* pbuffer = buffer;
// Maximum array dimension // Input buffer // Pointer to array buffer
cout << endl // Prompt for input << “Enter a string of less than “ << MAX << “ characters:”
189
Chapter 4 << endl; cin.getline(buffer, MAX, ‘\n’);
// Read a string until \n
while(*pbuffer) pbuffer++;
// Continue until \0
cout << endl << “The string \”” << buffer << “\” has “ << pbuffer - buffer << “ characters.”; cout << endl; return 0; }
Here’s an example of typical output from this example: Enter a string of less than 80 characters: The tigers of wrath are wiser than the horses of instruction. The string “The tigers of wrath are wiser than the horses of instruction.” has 61 characters.
How It Works Here the program operates using the pointer pbuffer rather than the array name buffer. You don’t need the count variable because the pointer is incremented in the while loop until ‘\0’ is found. When the ‘\0’ character is found, pbuffer will contain the address of that position in the string. The count of the number of characters in the string entered is therefore the difference between the address stored in the pointer pbuffer and the address of the beginning of the array denoted by buffer. You could also have incremented the pointer in the loop by writing the loop like this: while(*pbuffer++);
// Continue until \0
Now the loop contains no statements, only the test condition. This would work adequately, except for the fact that the pointer would be incremented after \0 was encountered, so the address would be one more than the last position in the string. You would therefore need to express the count of the number of characters in the string as pbuffer – buffer - 1. Note that here you can’t use the array name in the same way that you have used the pointer. The expression buffer++ is strictly illegal because you can’t modify the address value that an array name represents. Even though you can use an array name in an expression as though it is a pointer, it isn’t a pointer, because the address value that it represents is fixed.
Using Pointers with Multidimensional Arrays Using a pointer to store the address of a one-dimensional array is relatively straightforward, but with multidimensional arrays, things can get a little complicated. If you don’t intend to do this, you can skip this section as it’s a little obscure; however, if you have previous experience with C, this section is worth a glance.
190
Arrays, Strings, and Pointers If you have to use a pointer with multidimensional arrays, you need to keep clear in your mind what is happening. By way of illustration, you can use an array beans, declared as follows: double beans[3][4];
You can declare and assign a value to the pointer pbeans as follows: double* pbeans; pbeans = &beans[0][0];
Here you are setting the pointer to the address of the first element of the array, which is of type double. You could also set the pointer to the address of the first row in the array with the statement: pbeans = beans[0];
This is equivalent to using the name of a one-dimensional array which is replaced by its address. We used this in the earlier discussion; however, because beans is a two-dimensional array, you cannot set an address in the pointer with the following statement: pbeans = beans;
// Will cause an error!!
The problem is one of type. The type of the pointer you have defined is double*, but the array is of type double[3][4]. A pointer to store the address of this array must be of type double*[4]. C++ associates the dimensions of the array with its type and the statement above is only legal if the pointer has been declared with the dimension required. This is done with a slightly more complicated notation than you have seen so far: double (*pbeans)[4];
The parentheses here are essential; otherwise, you would be declaring an array of pointers. Now the previous statement is legal, but this pointer can only be used to store addresses of an array with the dimensions shown.
Pointer Notation with Multidimensional Arrays You can use pointer notation with an array name to reference elements of the array. You can reference each element of the array beans that you declared earlier, which had three rows of four elements, in two ways: ❑
Using the array name with two index values.
❑
Using the array name in pointer notation
Therefore, the following two statements are equivalent: beans[i][j] *(*(beans + i) + j)
Let’s look at how these work. The first line uses normal array indexing to refer to the element with offset j in row i of the array.
191
Chapter 4 You can determine the meaning of the second line by working from the inside, outwards. beans refers to the address of the first row of the array, so beans + i refers to row i of the array. The expression *(beans + i) is the address of the first element of row i, so *(beans + i) + j is the address of the element in row i with offset j. The whole expression therefore refers to the value of that element. If you really want to be obscure — and it isn’t recommended that you should be — the following two statements, where you have mixed array and pointer notation, are also legal references to the same element of the array: *(beans[i] + j) (*(beans + i))[j]
There is yet another aspect to the use of pointers that is really the most important of all: the ability to allocate memory for variables dynamically. You’ll look into that next.
Dynamic Memor y Allocation Working with a fixed set of variables in a program can be very restrictive. The need often arises within an application to decide the amount of space to be allocated for storing different types of variables at execution time, depending on the input data for the program. With one set of data it may be appropriate to use a large integer array in a program, whereas with a different set of input data, a large floating-point array may be required. Obviously, because any dynamically allocated variables can’t have been defined at compile time, they can’t be named in your source program. When they are created, they are identified by their address in memory, which is contained within a pointer. With the power of pointers and the dynamic memory management tools in Visual C++ 2005, writing your programs to have this kind of flexibility is quick and easy.
The Free Store, Alias the Heap In most instances, when your program is executed, there is unused memory in your computer. This unused memory is called the heap in C++, or sometimes the free store. You can allocate space within the free store for a new variable of a given type using a special operator in C++ that returns the address of the space allocated. This operator is new, and it’s complemented by the operator delete, which de-allocates memory previously allocated by new. You can allocate space in the free store for some variables in one part of a program, and then release the allocated space and return it to the free store after you have finished with the variables concerned. This makes the memory available for reuse by other dynamically allocated variables, later in the same program. You would want to use memory from the free store whenever you need to allocate memory for items that can only be determined at run-time. One example of this might be allocating memory to hold a string entered by the user of your application. There is no way you can know in advance how large this string needs to be, so you would allocate the memory for the string at run time, using the new operator. Later, you’ll look at an example of using the free store to dynamically allocate memory for an array, where the dimensions of the array are determined by the user at run time.
192
Arrays, Strings, and Pointers This can be a very powerful technique; it enables you to use memory very efficiently, and in many cases, it results in programs that can handle much larger problems, involving considerably more data than otherwise might be possible.
The new and delete Operators Suppose that you need space for a double variable. You can define a pointer to type double and then request that the memory be allocated at execution time. You can do this using the operator new with the following statements: double* pvalue = NULL; pvalue = new double;
// Pointer initialized with null // Request memory for a double variable
This is a good moment to recall that all pointers should be initialized. Using memory dynamically typically involves a number of pointers floating around, so it’s important that they should not contain spurious values. You should try to arrange that if a pointer doesn’t contain a legal address value, it is set to 0. The new operator in the second line of code above should return the address of the memory in the free store allocated to a double variable, and this address is stored in the pointer pvalue. You can then use this pointer to reference the variable using the indirection operator as you have seen. For example: *pvalue = 9999.0;
Of course, the memory may not have been allocated because the free store had been used up, or because the free store is fragmented by previous usage, meaning that there isn’t a sufficient number of contiguous bytes to accommodate the variable for which you want to obtain space. You don’t have to worry too much about this, however. With ANSI standard C++, the new operator will throw an exception if the memory cannot be allocated for any reason, which terminates your program. Exceptions are a mechanism for signaling errors in C++ and you’ll learn about these in Chapter 6. You can also initialize a variable created by new. Taking the example of the double variable that was allocated by new and the address stored in pvalue, you could have set the value to 999.0 as it was created with this statement: pvalue = new double(999.0);
// Allocate a double and initialize it
When you no longer need a variable that has been dynamically allocated, you can free up the memory that it occupies in the free store with the delete operator: delete pvalue;
// Release memory pointed to by pvalue
This ensures that the memory can be used subsequently by another variable. If you don’t use delete, and subsequently store a different address value in the pointer pvalue, it will be impossible to free up the memory or to use the variable that it contains because access to the address is lost. In this situation, you have what is referred to as a memory leak, especially when this situation recurs in your program.
193
Chapter 4
Allocating Memory Dynamically for Arrays Allocating memory for an array dynamically is very straightforward. If you wanted to allocate an array of type char, assuming pstr is a pointer to char, you could write the following statement: pstr = new char[20];
// Allocate a string of twenty characters
This allocates space for a char array of 20 characters and stores its address in pstr. To remove the array that you have just created in the free store, you must use the delete operator. The statement would look like this: delete [] pstr;
// Delete array pointed to by pstr
Note the use of square brackets to indicate that what you are deleting is an array. When removing arrays from the free store, you should always include the square brackets or the results are unpredictable. Note also that you do not specify any dimensions here, simply []. Of course, the pstr pointer now contains the address of memory that may already have been allocated for some other purpose so it certainly should not be used. When you use the delete operator to discard some memory that you previously allocated, you should always reset the pointer to 0, like this: pstr = 0;
Try It Out
// Set pointer to null
Using Free Store
You can see how dynamic memory allocation works in practice by rewriting the program that calculates an arbitrary number of primes, this time using memory in the free store to store them. // Ex4_11.cpp // Calculating primes using dynamic memory allocation #include #include using std::cin; using std::cout; using std::endl; using std::setw; int main() { long* pprime = 0; long trial = 5; int count = 3; int found = 0; int max = 0;
// // // // //
Pointer to prime array Candidate prime Count of primes found Indicates when a prime is found Number of primes required
cout << endl << “Enter the number of primes you would like (at least 4): “; cin >> max; // Number of primes required if(max < 4)
194
// Test the user input, if less than 4
Arrays, Strings, and Pointers max = 4;
// ensure it is at least 4
pprime = new long[max]; *pprime = 2; *(pprime + 1) = 3; *(pprime + 2) = 5;
// Insert three // seed primes
do { trial += 2; // Next value for checking found = 0; // Set found indicator for(int i = 0; i < count; i++) // Division by existing primes { found =(trial % *(pprime + i)) == 0;// True for exact division if(found) // If division is exact break; // it’s not a prime } if (found == 0) // We got one... *(pprime + count++) = trial; // ...so save it in primes array } while(count < max); // Output primes 5 to a line for(int i = 0; i < max; i++) { if(i % 5 == 0) // New line on 1st, and every 5th line cout << endl; cout << setw(10) << *(pprime + i); } delete [] pprime; // Free up memory pprime = 0; // and reset the pointer cout << endl; return 0; }
Here’s an example of the output from this program: Enter the number of primes you would like (at least 4): 20 2 13 31 53
3 17 37 59
5 19 41 61
7 23 43 67
11 29 47 71
How It Works In fact, the program is similar to the previous version. After receiving the number of primes required in the int variable max, you allocate an array of that size in the free store using the operator new. Note that you have made sure that max can be no less than 4. This is because the program requires space to be allocated in the free store for at least the three seed primes, plus one new one. You specify the size of the array that is required by putting the variable max between the square brackets following the array type specification: pprime = new long[max];
195
Chapter 4 You store the address of the memory area that is allocated by new in the pointer pprime. The program would terminate at this point if the memory could not be allocated. After the memory that stores the prime values has been successfully allocated, the first three array elements are set to the values of the first three primes: *pprime = 2; *(pprime + 1) = 3; *(pprime + 2) = 5;
// Insert three // seed primes
You are using the dereference operator to access the first three elements of the array. As you saw earlier, the parentheses in the second and third statements because the precedence of the * operators is higher than that of the + operator. You can’t specify initial values for elements of an array that you allocate dynamically. You have to use explicit assignment statements if you want to set initial values for elements of the array. The calculation of the prime numbers is exactly as before; the only change is that the name of the pointer you have here, pprime, is substituted for the array name, primes, that you used in the previous version. Equally, the output process is the same. Acquiring space dynamically is really not a problem at all. After it has been allocated, it in no way affects how the computation is written. After you finish with the array, you remove it from the free store using the delete operator, remembering to include the square brackets to indicate that it is an array you are deleting. delete [] pprime;
// Free up memory
Although it’s not essential here, you also set the pointer to 0: pprime = 0;
// and reset the pointer
All memory allocated in the free store is released when your program ends, but it is good to get into the habit of resetting pointers to 0 when they no longer point to valid memory areas.
Dynamic Allocation of Multidimensional Arrays Allocating memory in the free store for a multidimensional array involves using the new operator in a slightly more complicated form than that for a one-dimensional array. Assuming that you have already declared the pointer pbeans appropriately, to obtain the space for the array beans[3][4] that you used earlier in this chapter, you could write this: pbeans = new double [3][4];
// Allocate memory for a 3x4 array
You just specify both array dimensions between square brackets after the type name for the array elements. Allocating space for a three-dimensional array simply requires that you specify the extra dimension with new, as in this example: pBigArray = new double [5][10][10]; // Allocate memory for a 5x10x10 array
196
Arrays, Strings, and Pointers However many dimensions there are in the array that has been created, to destroy it and release the memory back to the free store you write the following: delete [] pBigArray;
// Release memory for array
You always use just one pair of square brackets following the delete operator, regardless of the dimensionality of the array with which you are working. You have already seen that you can use a variable as the specification of the dimension of a one-dimensional array to be allocated by new. This extends to two or more dimensions but with the restriction that only the leftmost dimension may be specified by a variable. All the other dimensions must be constants or constant expressions. So you could write this: pBigArray = new double[max][10][10];
where max is a variable; however, specifying a variable for any dimension other than the left most causes an error message to be generated by the compiler.
Using References A reference appears to be similar to a pointer in many respects, which is why I’m introducing it here, but it really isn’t the same thing at all. The real importance of references becomes apparent only when you get to explore their use with functions, particularly in the context of object-oriented programming. Don’t be misled by its simplicity and what might seem to be a trivial concept. As you will see later, references provide some extraordinarily powerful facilities and in some contexts enable you to achieve results that would be impossible without using them.
What Is a Reference? A reference is an alias for another variable. It has a name that can be used in place of the original variable name. Because it is an alias and not a pointer, the variable for which it is an alias has to be specified when the reference is declared, and unlike a pointer, a reference can’t be altered to represent another variable.
Declaring and Initializing References Suppose that you have declared a variable as follows: long number = 0;
You can declare a reference for this variable using the following declaration statement: long& rnumber = number;
// Declare a reference to variable number
The ampersand following the type name long and preceding the variable name rnumber, indicates that a reference is being declared and the variable name it represents, number, is specified as the initializing
197
Chapter 4 value following the equals sign; therefore, the variable rnumber is of type ‘reference to long’. You can now use the reference in place of the original variable name. For example, this statement, rnumber += 10;
has the effect of incrementing the variable number by 10. Let’s contrast the reference rnumber with the pointer pnumber, declared in this statement: long* pnumber = &number;
// Initialize a pointer with an address
This declares the pointer pnumber, and initializes it with the address of the variable number. This then allows the variable number to be incremented with a statement such as: *pnumber += 10;
// Increment number through a pointer
There is a significant distinction between using a pointer and using a reference. The pointer needs to be de-referenced and whatever address it contains is used to access the variable to participate in the expression. With a reference, there is no need for de-referencing. In some ways, a reference is like a pointer that has already been de-referenced, although it can’t be changed to reference another variable. The reference is the complete equivalent of the variable for which it is a reference. A reference may seem like just an alternative notation for a given variable, and here it certainly appears to behave like that. However, you’ll see when I discuss functions in C++ that this is not quite true and that it can provide some very impressive extra capabilities.
C++/CLI Programming Dynamic memory allocation works differently with the CLR, and the CLR maintains its own memory heap that is independent of the native C++ heap. The CLR automatically deletes memory that you allocate on the CLR heap when it is no longer required, so you do not need to use the delete operator in a program written for the CLR. The CLR may also compact heap memory to avoid fragmentation from time to time. Thus at a stroke, the CLR eliminates the possibility of memory leaks and memory fragmentation. The management and clean-up of the heap that the CLR provides is described as garbage collection(the garbage being your discarded variables and objects and the heap that is managed by the CLR is called the garbage-collected heap. You use the gcnew operator instead of new to allocate memory in a C++/CLI, program and the ‘gc’ prefix is a cue to the fact that you are allocating memory on the garbagecollected heap and not the native C++ heap where all the housekeeping is down to you. The CLR garbage collector is able to delete objects and release the memory that they occupy when they are no longer required. An obvious question arises: how does the garbage collector know when an object on the heap is no longer required? The answer is quite simple; the CLR keeps track of every variable that references each object in the heap and when there are no variables containing the address of a given object, the object can no longer be referred to in a program and therefore can be deleted. Because the garbage collection process can involving compacting of the heap memory area to remove fragmented unused blocks of memory, the addresses of data item that you have stored in the heap can change. Consequently you cannot use ordinary native C++ pointers with the garbage-collected heap because if the location of the data pointed to changes, the pointer will no longer be valid. You need a
198
Arrays, Strings, and Pointers way to access objects on the heap that enables the address to be updated when the garbage collector relocates the data item in the heap. This capability is provided in two ways: by a tracking handle (also referred to simply as a handle) that is analogous to a pointer in native C++ and by a tracking reference that provides the equivalent of a native C++ reference in a CLR program.
Tracking Handles A tracking handle has similarities to a native C++ pointer but there are significant differences, too. A tracking handle does store an address, and the address it contains is automatically updated by the garbage collector if the object it references is moved during compaction of the heap. However, you cannot perform address arithmetic with a tracking pointer as you can with a native pointer and casting of a tracking handle is not permitted All objects created in the CLR heap must be referenced by a tracking handle. All objects that are reference class types are stored in the heap and therefore the variables you create to refer to such objects must be tracking handles. For instance, the String class type is a reference class type so variables that reference String objects must be tracking handles. The memory for value class types is allocated on the stack by default, but you can choose to store values in the heap by using the gcnew operator. This is also a good time to remind you of a point I mentioned in Chapter 2(that variables allocated on the CLR heap, which includes all CLR reference types, cannot be declared at global scope.
Declaring Tracking Handles You specify a handle for a type by placing the ^ symbol (commonly referred to as a ‘hat’) following the type name. For example, here’s how you could declare a tracking handle with the name proverb that can store the address of a String object: String^ proverb;
This defines the variable proverb to be a tracking handle of type String^. When you declare a handle it is automatically initialized with null, so it will not refer to anything. To explicitly set a handle to null you use the keyword nullptr like this: proverb = nullptr;
// Set handle to null
Note that you cannot use 0 to represent null here, as you can with native pointers. If you initialize a handle with 0, the value 0 is converted to the type of object that the handles references and the address of this new object is stored in the handle. Of course, you can initialize a handle explicitly when you declare it. Here’s another statement that defines a handle to a String object String^ saying = L”I used to think I was indecisive but now I’m not so sure”;
This statement creates a String object on the heap that contains the string on the right of the assignment; the address of the new object is stored in saying. Note that the type of the string literal is const wchar_t*, not type String. The way the String class has been defined makes it possible for such a literal to be used to create an object of type String.
199
Chapter 4 Here’s how you could create a handle for a value type: int^ value = 99;
This statement creates the handle value of type int^ and the value it points to on the heap is initialized to 99. Remember that you have created a kind of pointer so value cannot participate in arithmetic operations without dereferencing it. To dereference a tracking handle you use the * operator in the same way as for native pointers. For example, here is a statement that uses the value pointed to by a tracking handle in an arithmetic operation: int result = 2*(*value)+15;
the expression *value between the parentheses accesses the integer stored at the address held in the tracking handle so the variable result is set to 213. Note that when you use a handle of the right of an assignment, there’s no need to explicitly dereference it to store a result; the compiler takes care of it for you. For example: int^ result = 0; result = 2*(*value)+15;
Here you first create the handle result that points to a value 0 on the heap. Note that this results in a warning from the compiler because it deduces that you might intend to initialize the handle to null and this is not the way to do that. Because result appears on the left of an assignment in the next statement and the right hand side produces a value, the compiler is able to determine that result must be dereferenced to store the value. Of course, you could write it explicitly like this: *result = 2*(*value)+15;
Note that this works only if result has actually been defined. If result has only been declared, when you execute the code you get a runtime error. For example: int^ result; *result = 2*(*value)+15;
// Declaration but no definition // Error message - unhandled exception
Because you are derefencing result in the second statement, you are implying that the object pointed to already exists. Because this is not the case you get a run-time error. The first statement is a declaration of the handle, result, and this is set to null by default and you cannot dereference a null handle. If you don’t explicitly dereference result in the second statement, everything works as it should because the result of the expression on the right of the assignment is a value class type and its address is stored in result.
CLR Arrays CLR arrays are different from the native C++ arrays. Memory for a CLR array is allocated on the garbage-collected heap, but there’s more to it than that. CLR arrays have built-in functionality that you don’t get with native C++ arrays, as you’ll see shortly. You specify an array variable type using the keyword array. You must also specify the type for the array elements between angled brackets following the array keyword, so the general form for a variable to reference a one-dimensional array is
200
Arrays, Strings, and Pointers array<element_type>^. Because a CLR array is created on the heap, an array variable is always a tracking handle. Here’s an example of a declaration for an array variable: array^ data;
The array variable, data, can store a reference to any one-dimensional array of elements of type int. You can create a CLR array using the gcnew operator at the same time you declare the array variable: array^ data = gcnew array(100);
// Create an array to store 100 integers
This statement creates a one-dimensional array with the name data(note that an array variable is a tracking handle, so you must not forget the hat following the element type specification between the angled brackets. The number of elements appears between parentheses following the array type specification so this array contains 100 elements each of which can store a value of type int. Similar to native C++ arrays, CLR array elements are indexed from zero so you could set values for the elements in the data array like this: for(int i = 0 ; i<100 ; i++) data[i] = 2*(i+1);
This loop sets the values of the elements to 2, 4, 6, and so on up to 200. Elements in a CLR array are objects so here you are storing objects of type Int32 in the array. Of course, these behave like ordinary integers in arithmetic expressions so the fact that they are objects is transparent in such situations. In the previous loop, the number of elements appears in the loop as a literal value. It would be better to use the Length property of the array that records the number of elements, like this: for(int i = 0 ; i < data->Length ; i++) data[i] = 2*(i+1);
To access the Length property, you use the -> operator because data is a tracking handle and works like a pointer. The Length property records the number of values as a 32-bit integer value. If you need it, you can get the array length as a 64-bit value through the LongLength property. You can also iterate over all the elements in an array using the for each loop: array^ values = { 3, 5, 6, 8, 6}; for each(int item in values) { item = 2*item + 1; Console::Write(“{0,5}”,item); }
Within the loop, item references each of the elements in the values array in turn. The first statement in the body of the loop replaces the current element’s value by twice the value plus 1. The second statement in the loop outputs the new value right-justified in a field width of five characters so the output produced by this code fragment is: 7
11
13
17
13
201
Chapter 4 An array variable can store the address of any array of the same rank (the rank being the number of dimensions which in the case of the data array is 1) and element type. For example: data = gcnew array(45);
This statement creates a new one-dimensional array of 45 elements of type int and stores its address in data. The original array is discarded. You can also create an array from a set of initial values for the elements: array<double>^ samples = { 3.4, 2.3, 6.8, 1.2, 5.5, 4.9. 7.4, 1.6};
The size of the array is determined by the number of initial values between the braces, in this case 8, and the values are assigned to the elements in sequence. Of course, the elements in an array can be of any type, so you can easily create an array of strings: array<String^>^ names = { “Jack”, “Jane”, “Joe”, “Jessica”, “Jim”, “Joanna”};
The elements of this array are initialized with the strings that appear between the braces, and the number of strings determines the number of elements in the array. String objects are created on the CLR heap so the element type is a tracking handle type, String^. If you declare the array variable without initializing it, you must explicitly create the array if you want to use a list of initial values. For example: array<String^>^ names; // Declare the array variable names = gcnew array<String^>{ “Jack”, “Jane”, “Joe”, “Jessica”, “Jim”, “Joanna”};
The second statement creates the array and initializes it with the strings between the braces. Without the explicit gcnew definition the statement will not compile. You can use the static Clear() function that is defined in the Array class to set any sequence of numeric elements in an array to zero. You call a static function using the class name and you’ll learn more about such functions when we explore classes in detail. Here’s an example of how you could use the Clear() function: Array::Clear(samples, 0, samples->Length);
// Set all elements to zero
The first argument to Clear() is the array that is to be cleared, the second argument is the index for the first element to be cleared, and the third argument is the number of elements to be cleared. Thus this example sets all the elements of the samples array to 0.0. If you apply the Clear() function to an array of tracking handles such as String^, the elements are set to null, and if you apply it to an array of bool elements they are set to false. It’s time to let a CLR array loose in an example.
202
Arrays, Strings, and Pointers Try It Out
Using a CLR Array
In this example, you generate an array containing random values and then find the maximum value. Here’s the code: // Ex4_12.cpp : main project file. // Using a CLR array #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { array<double>^ samples = gcnew array<double>(50); // Generate random element values Random^ generator = gcnew Random; for(int i = 0 ; i< samples->Length ; i++) samples[i] = 100.0*generator->NextDouble(); // Output the samples Console::WriteLine(L”The array contains the following values:”); for(int i = 0 ; i< samples->Length ; i++) { Console::Write(L”{0,10:F2}”, samples[i]); if((i+1)%5 == 0) Console::WriteLine(); } // Find the maximum value double max = 0; for each(double sample in samples) if(max < sample) max = sample; Console::WriteLine(L”The maximum value in the array is {0:F2}”, max); return 0; }
Typical output from this example looks like this: The array contains the following values: 30.38 73.93 29.82 93.00 89.53 75.87 5.98 45.29 5.25 53.86 11.40 3.34 69.94 82.43 43.05 32.87 58.89 96.69 34.67 18.81 89.60 25.53 34.00 97.35 52.64 90.85 10.35 46.14 55.46 93.26 92.96 85.11 50.50 8.10 29.32 82.98 83.94 56.95 15.04 21.94 The maximum value in the array is 97.35 Press any key to continue . . .
78.14 89.83 83.39 59.50 72.99 55.26 82.03 10.55 76.48 24.81
203
Chapter 4 How It Works You first create an array that stores 50 values of type double: array<double>^ samples = gcnew array<double>(50);
the array variable, samples must be a tracking handle because CLR arrays are created on the garbagecollected heap. You populate the array with pseudo-random values of type double with the following statements: Random^ generator = gcnew Random; for(int i = 0 ; i< samples->Length ; i++) samples[i] = 100.0*generator->NextDouble();
The first statement creates an object of type Random on the CLR heap. A Random object has functions that will generate pseudo-random values. Here you use the NextDouble() function in the loop, which returns a random value of type double that lies between 0.0 and 1.0. By multiplying this by 100.0 you get a value between 0.0 and 100.0. The for loop stores a random value in each element of the samples array. A Random object also has a Next() function that returns a random non-negative value of type int. If you supply an integer argument when you call the Next() function, it will return a random non-negative integer less than the argument value. You can also supply two integer arguments that represent the minimum and maximum values for the random integer to be returned. The next loop outputs the contents of the array five elements to a line: Console::WriteLine(L”The array contains the following values:”); for(int i = 0 ; i< samples->Length ; i++) { Console::Write(L”{0,10:F2}”, samples[i]); if((i+1)%5 == 0) Console::WriteLine();
Within the loop you write the value each element with a field width of 10 and 2 decimal places. Specifying the field width ensures the values align in columns. You also write a newline character to the output whenever the expression (i+1)%5 is zero, which is after every fifth element value so you get five to a line in the output. Finally you figure out what the maximum element value is: double max = 0; for each(double sample in samples) if(max < sample) max = sample;
This uses a for each loop just to show that you can. The loop compares max with each element value in turn and whenever the element is greater than the current value in max, max is set to that value so you end up with the maximum element value in max.
204
Arrays, Strings, and Pointers You could use a for loop here if you also wanted to record the index position of the maximum element as well as its value(for example: double max = 0; int index = 0; for (int i = 0 ; i < sample->Length ; i++) if(max < samples[i]) { max = samples[i]; index = i; }
Sorting One-Dimensional Arrays The Array class in the System namespace defines a Sort() function that sorts the elements of a onedimensional array so that they are in ascending order. To sort an array you just pass the array handle to the Sort() function. Here’s an example: array^ samples = { 27, 3, 54, 11, 18, 2, 16}; Array::Sort(samples); // Sort the array elements for each(int value in samples) Console::Write(L”{0, 8}”, value); Console::WriteLine();
// Output the array elements
The call to the Sort() function rearranges the values of the elements in the samples array so they are in ascending sequence. The result of executing this code fragment is: 2
3
11
16
18
27
54
You can also sort a range of elements in an array by supplying two more arguments to the Sort() function specifying the index for the first element of those to be sorted and the number of elements to be sorted. For example: array^ samples = { 27, 3, 54, 11, 18, 2, 16}; Array::Sort(samples, 2, 3); // Sort elements 2 to 4
This statement sorts the three elements in the samples array that begin at index position 2. After executing these statements, the elements in the array will have the values: 27
3
11
18
54
2
16
The are several other versions of the Sort() function that you can find if you consult the documentation but I’ll introduce one other that is particularly useful. This version presumes you have two arrays that are associated so that the elements in the first array represent keys to the corresponding elements in the second array. For example, you might store names of people in one array and the weights of the individuals in a second array. The Sort() function sorts the array of names in ascending sequence and also rearrange the elements of the weights array so the weights still match the appropriate person. Let’s try it in an example.
205
Chapter 4 Try It Out
Sorting Two Associated Arrays
This example creates an array of names and stores the weights of each person in the corresponding element of a second array. It then sorts both arrays in a single operation. Here’s the code: // Ex4_13.cpp : main project file. // Sorting an array of keys(the names) and an array of objects(the weights) #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { array<String^>^ names = { “Jill”, “Ted”, “Mary”, “Eve”, “Bill”, “Al”}; array^ weights = { 103, 168, 128, 115, 180, 176}; Array::Sort( names,weights); for each(String^ name in names) Console::Write(L”{0, 10}”, name); Console::WriteLine();
// Sort the arrays // Output the names
for each(int weight in weights) Console::Write(L”{0, 10}”, weight); Console::WriteLine(); return 0;
// Output the weights
}
The output from this program is: Al Bill Eve 176 180 115 Press any key to continue . . .
Jill 103
Mary 128
Ted 168
How It Works The values in the weights array correspond to the weight of the person at the same index position in the names array. The Sort() function you call here sorts both arrays using the first array argument(names in this instance(to determine the order of both arrays. You can see from that output that after sorting everyone still has his or her correct weight recorded in the corresponding element of the weights array.
Searching One-Dimensional Arrays The Array class also provides functions that search the elements of a one-dimensional array. Versions of the BinarySearch() function uses a binary search algorithm to find the index position of a given element in the entire array, or from a given range of elements. The binary search algorithm requires that the elements are ordered if it is to work, so you need to sort the elements before searching an array.
206
Arrays, Strings, and Pointers Here’s how you could search an entire array: array^ values = { 23, 45, 68, 94, 123, 127, 150, 203, 299}; int toBeFound = 127; int position = Array::BinarySearch(values, toBeFound); if(position<0) Console::WriteLine(L”{0} was not found.”, toBeFound); else Console::WriteLine(L”{0} was found at index position {1}.”, toBeFound, position);
The value to be found is stored in the toBeFound variable. The first argument to the BinarySearch() function is the handle of the array to be searched and the second argument specifies what you are looking for. The result of the search is returned by the BinarySearch() function as a value of type int. If the second argument to the function is found in the array specified by the first argument, its index position is returned; otherwise a negative integer is returned. Thus you must test the value returned to determine whether or not the search target was found. Because the values in the values array are already in ascending sequence there is no need to sort the array before searching it. This code fragment would produce the output: 127 was found at index position 5.
To search a given range of elements in an array you use a version of the BinarySearch() function that accepts four arguments. The first argument is the handle of the array to be searched, the second argument is the index position of the element where the search should start, the third argument is the number of elements to be searched, and the fourth argument is what you are looking for. Here’s how you might use that: array^ values = { 23, 45, 68, 94, 123, 127, 150, 203, 299}; int toBeFound = 127; int position = Array::BinarySearch(values, 3, 6, toBeFound);
This searches the values array from the fourth array element through to the last. As with the previous version of BinarySearch(), the function returns the index position found or a negative integer if the search fails. Let’s try a searching example.
Try It Out
Searching Arrays
This is a variation on the previous example with a search operation added: // Ex4_14.cpp : main project file. // Searching an array #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) {
207
Chapter 4 array<String^>^ names = { “Jill”, “Ted”, “Mary”, “Eve”, “Bill”, “Al”, “Ned”, “Zoe”, “Dan”, “Jean”}; array^ weights = { 103, 168, 128, 115, 180, 176, 209, 98, 190, 130 }; array<String^>^ toBeFound = {“Bill”, “Eve”, “Al”, “Fred”}; Array::Sort( names, weights);
// Sort the arrays
int result = 0; for each(String^ name in toBeFound) { result = Array::BinarySearch(names, name);
// Stores search result // Search to find weights // Search names array
if(result<0) // Check the result Console::WriteLine(L”{0} was not found.”, name); else Console::WriteLine(L”{0} weighs {1} lbs.”, name, weights[result]); } return 0; }
This program produces the output: Bill weighs 180 lbs. Eve weighs 115 lbs. Al weighs 176 lbs. Fred was not found. Press any key to continue . . .
How It Works You create two associated arrays(an array of names and an array of corresponding weights in pounds. You also create the toBeFound array that contains the names of the people for whom you’d like to know their weights. You sort the names and weights arrays using the names array to determine the order. You then search the names array for each name in the toBeFound array in a for each loop. The loop variable, name, is assigned each of the names in the toBeFound array in turn. Within the loop, you search for the current name with the statement: result = Array::BinarySearch(names, name);
// Search names array
This returns the index of the element from names that contain name or a negative integer if the name is not found. You then test the result and produce the output in the if statement: if(result<0) // Check the result Console::WriteLine(L”{0} was not found.”, name); else Console::WriteLine(L”{0} weighs {1} lbs.”, name, weights[result]);
Because the ordering of the weights array was determined by the ordering of the names array, you are able to index the weights array with result, the index position in the names array where name was found. You can see from the output that “Fred” was not found in the names array.
208
Arrays, Strings, and Pointers When the binary search operation fails, the value returned is not just any old negative value. It is in fact the bitwise complement of the index position of the first element that is greater than the object you are searching for, or the bitwise complement of the Length property of the array if no element is greater than the object sought. Knowing this you can use the BinarySearch() function to work out where you should insert a new object in an array and still maintain the order of the elements. Suppose you wanted to insert “Fred” in the names array. You can find the index position where it should be inserted with these statements: array<String^>^ names = { “Jill”, “Ted”, “Mary”, “Eve”, “Bill”, “Al”, “Ned”, “Zoe”, “Dan”, “Jean”}; Array::Sort(names); // Sort the array String^ name = L”Fred”; int position = Array::BinarySearch(names, name); if(position<0) // If it is negative position = ~position; // flip the bits to get the insert index
If the result of the search is negative, flipping all the bits gives you the index position of where the new name should be inserted. If the result is positive, the new name is identical to the name at this position, so you can use the result as the new position directly. You can now copy the names array into a new array that has one more element and use the position value to insert name at the appropriate place: array<String^>^ newNames = gcnew array<String^>(names->Length+1); // Copy elements from names to newNames for(int i = 0 ; i<position ; i++) newNames[i] = names[i]; newNames[position] = name;
// Copy the new element
if(positionLength) for(int i = position ; iLength ; i++) newNames[i+1] = names[i];
// If any elements remain in names // copy them to newNames
This creates a new array with a length one greater than the old array. You then copy all the elements from the old to the new up to index position position-1. You then copy the new name followed by the remaining elements from the old array. To discard the old array, you would just write: names = nullptr;
Multidimensional Arrays You can create arrays that have two or more dimensions; the maximum number of dimensions an array can have is 32, which should accommodate most situations. You specify the number of dimensions that your array has between the angled brackets immediately following the element type and separated from it by a comma. The dimension of an array is 1 by default, which is why you did not need to specify up to now. Here’s how you can create a two-dimensional array of integer elements: array^ values = gcnew array(4, 5);
209
Chapter 4 This statement creates a two-dimensional array with four rows and five columns so it has a total of 20 elements. To access an element of a multidimensional array you specify a set of index values, one for each dimension; these are place between square brackets separated by commas following the array name. Here’s how you could set values for the elements of a two-dimensional array of integers: int nrows = 4; int ncols = 5; array^ values = gcnew array(nrows, ncols); for(int i = 0 ; i
The nested loop iterates over all the elements of the array. The outer loop iterates over the rows and the inner loop iterates over every element in the current row. As you see, each element is set to a value that is given by the expression (i+1)*(j+1) so elements in the first row will be set to 1,2,3,4,5, elements in the second row will be 2,4,6,8,10, and so on through to the last row which will be 4,6,12,16,20. I’m sure you will have noticed that the notation for accessing an element of a two-dimensional array here is different from the notation used for native C++ arrays. This is no accident. A C++/CLI array is not an array of arrays like a native C++ array it is a true two-dimensional array. You cannot use a single index with two-dimensional C++/CLI array, because this has no meaning; the array is a two-dimensional array of elements, not an array of arrays. As I said earlier, the dimensionality of an array is referred to as its rank, so the rank of the values array in the previous fragment is 2. Of course you can also define C++/CLI arrays of rank 3 or more, up to an array of rank 32. In contrast, native C++ arrays are actually always of rank 1 because native C++ arrays of two or more dimensions are really arrays of arrays. As you’ll see later, you can also define arrays of arrays in C++/CLI. Let’s put a multidimensional array to use in an example.
Try It Out
Using a Multidimensional Array
This CLR console example creates a 12x12 multiplication table in a two-dimensional array: // Ex4_15.cpp : main project file. // Using a two-dimensional array #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { const int SIZE = 12; array^ products = gcnew array(SIZE,SIZE); for (int i = 0 ; i < SIZE ; i++) for(int j = 0 ; j < SIZE ; j++) products[i,j] = (i+1)*(j+1); Console::WriteLine(L”Here is the {0} times table:”, // Write horizontal divider line
210
SIZE);
Arrays, Strings, and Pointers for(int i = 0 ; i <= SIZE ; i++) Console::Write(L”_____”); Console::WriteLine();
// Write newline
// Write top line of table Console::Write(L” |”); for(int i = 1 ; i <= SIZE ; i++) Console::Write(L”{0,3} |”, i); Console::WriteLine();
// Write newline
// Write horizontal divider line with verticals for(int i = 0 ; i <= SIZE ; i++) Console::Write(L”____|”); Console::WriteLine(); // Write newline // Write remaining lines for(int i = 0 ; i<SIZE ; i++) { Console::Write(L”{0,3} |”, i+1); for(int j = 0 ; j<SIZE ; j++) Console::Write(L”{0,3} |”, products[i,j]); Console::WriteLine();
// Write newline
} // Write horizontal divider line for(int i = 0 ; i <= SIZE ; i++) Console::Write(L”_____”); Console::WriteLine();
// Write newline
return 0; }
This example should produce the following output: Here is the 12 times table: _________________________________________________________________ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ____|____|____|____|____|____|____|____|____|____|____|____|____| 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 2 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 3 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 4 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 5 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 6 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 60 | 66 | 72 | 7 | 7 | 14 | 21 | 28 | 35 | 42 | 49 | 56 | 63 | 70 | 77 | 84 | 8 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 9 | 9 | 18 | 27 | 36 | 45 | 54 | 63 | 72 | 81 | 90 | 99 |108 | 10 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |100 |110 |120 | 11 | 11 | 22 | 33 | 44 | 55 | 66 | 77 | 88 | 99 |110 |121 |132 | 12 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 |108 |120 |132 |144 | _________________________________________________________________ Press any key to continue . . .
211
Chapter 4 How It Works It looks like a lot of code, but most of it is concerned with making the output pretty. You create the twodimensional array with the following statements: const int SIZE = 12; array^ products = gcnew array(SIZE,SIZE);
The first line defines a constant integer value that specifies the number of elements in each array dimension. The second line defines an array of rank 2 that has 12 rows of 12 elements. This array stores the products in the 12x12 table. You set the values of the elements in the products array in a nested loop: for (int i = 0 ; i < SIZE ; i++) for(int j = 0 ; j < SIZE ; j++) products[i,j] = (i+1)*(j+1);
The outer loop iterates over the rows, and the inner loop iterates over the columns. The value of each element is the product of the row and column index values after they are incremented by 1. The rest of the code in main() is concerned solely with generating output. After writing the initial table heading, you create a row of bars to mark the top of the table like this: for(int i = 0 ; i <= SIZE ; i++) Console::Write(L”_____”); Console::WriteLine();
// Write newline
Each iteration of the loop writes five horizontal bar characters. Note that the upper limit for the loop is inclusive, so you write 13 sets of five bars to allow for the row labels in the table plus the 12 columns. Next you write the row of column labels for the table with another loop: // Write top line of table Console::Write(L” |”); for(int i = 1 ; i <= SIZE ; i++) Console::Write(L”{0,3} |”, i); Console::WriteLine();
// Write newline
You have to write the space over the row label position separately because that is a special case with no output value. Each of the column labels is written in the loop. You then write a newline character ready for the row outputs that follow. The row outputs are written in a nested loop: for(int i = 0 ; i<SIZE ; i++) { Console::Write(L”{0,3} |”, i+1); for(int j = 0 ; j<SIZE ; j++) Console::Write(L”{0,3} |”, products[i,j]); Console::WriteLine(); }
212
// Write newline
Arrays, Strings, and Pointers The outer loop iterates over the rows and the code inside the outer loop writes a complete row, including the row label on the left. The inner loop writes the values from the products array that correspond to the ith row, with the values separated by vertical bars. The remaining code writes more horizontal bars to finish off the bottom of the table.
Arrays of Arrays Array elements can be of any type so you can create arrays where the elements are tracking handles that reference arrays. This gives you the possibility of creating so-called jagged arrays because each handle referencing an array can have a different number of elements. This is most easily understood by looking at an example. Suppose you want to store the names of children in a class grouped by the grade they scored, where there are five classifications corresponding to grades A, B, C, D, and E. You could first create an array of five elements where each element stores an array of names. Here’s the statement that will do that: array< array< String^ >^ >^ grades = gcnew array< array< String^ >^ >(5);
Don’t let all the hats confuse you(it’s simpler than it looks. The array variable, grades, is a handle of type array^. Each element in the array is also a handle to an array, so the type of the array elements is of the same form(array^ so this has to go between the angled brackets in the original array type specification, which results in array< array^ >^. The elements stored in the arrays are also handles to String objects so you must replace type in the last expression by String^; thus you end up with the array type being array< array< String^ >^ >^. With the array of arrays worked out, you can now create the arrays of names. Here’s an example of what that might look like: grades[0] grades[1] grades[2] grades[3] grades[4]
= = = = =
gcnew gcnew gcnew gcnew gcnew
array<String^>{“Louise”, “Jack”}; array<String^>{“Bill”, “Mary”, “Ben”, “Joan”}; array<String^>{“Jill”, “Will”, “Phil”}; array<String^>{“Ned”, “Fred”, “Ted”, “Jed”, “Ed”}; array<String^>{“Dan”, “Ann”};
// // // // //
Grade Grade Grade Grade Grade
A B C D E
The expression grades[n] accesses the nth element is the grades array and of course this is a handle to an array of String^ handles in each case. Thus each of the five statements creates an array of String object handles and stores the address in one of the elements of the grades array. As you see, the arrays of strings vary in length, so clearly you can manage a set of arrays with arbitrary lengths in this way. You could create and initialize the whole array of arrays in a single statement: array< array< String^ >^ >^ grades = gcnew array< array< String^ >^ > { gcnew array<String^>{“Louise”, “Jack”}, // gcnew array<String^>{“Bill”, “Mary”, “Ben”, “Joan”}, // gcnew array<String^>{“Jill”, “Will”, “Phil”}, // gcnew array<String^>{“Ned”, “Fred”, “Ted”, “Jed”, “Ed”}, // gcnew array<String^>{“Dan”, “Ann”} // };
Grade Grade Grade Grade Grade
A B C D E
The initial values for the elements are between the braces.
213
Chapter 4 Let’s put this in a working example that demonstrates how you can process arrays of arrays.
Try It Out
Using an Array of Arrays
Create a CLR console program project and modify it as follows: // Ex4_16.cpp : main project file. // Using an array of arrays #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { array< array< String^ >^ >^ grades = gcnew array< array< String^ >^ > { gcnew array<String^>{“Louise”, “Jack”}, // gcnew array<String^>{“Bill”, “Mary”, “Ben”, “Joan”}, // gcnew array<String^>{“Jill”, “Will”, “Phil”}, // gcnew array<String^>{“Ned”, “Fred”, “Ted”, “Jed”, “Ed”}, // gcnew array<String^>{“Dan”, “Ann”} // };
Grade Grade Grade Grade Grade
wchar_t gradeLetter = ‘A’; for each(array< String^ >^ grade in grades) { Console::WriteLine(“Students with Grade {0}:”, gradeLetter++); for each( String^ student in grade) Console::Write(“{0,12}”,student);
// Output the current name
Console::WriteLine(); } return 0;
// Write a newline
}
This example produces the following output: Students with Louise Students with Bill Students with Jill Students with Ned Students with Dan Press any key
214
Grade A: Jack Grade B: Mary Ben Grade C: Will Phil Grade D: Fred Ted Grade E: Ann to continue . . .
Joan
Jed
Ed
A B C D E
Arrays, Strings, and Pointers How It Works The array definition is exactly as you saw in the previous section. Next you define the gradeLetter variable as type wchar_t with the initial value ‘A’. This is to be used to present the grade classification in the output. The students and their grades are listed by the nested loops. The outer for each loop iterates over the elements in the grades array: for each(array< String^ >^ grade in grades) { // Process students in the current grade... }
The loop variable, grade, is of type array< String^ >^ because that’s the element type in the grades array. The variable grade references each of the arrays of String^ handles in turn, so first time around the loop it references the array of grade A student names, second time around it references grade B student names, and so on until the last loop iteration when it references the grade E student names. On each iteration of the outer loop, you execute the following code: Console::WriteLine(“Students with Grade {0}:”, gradeLetter++); for each( String^ student in grade) Console::Write(“{0,12}”,student);
// Output the current name
Console::WriteLine();
// Write a newline
The first statement writes a line that includes the current value of gradeLetter, which starts out as ‘A’. The statement also increments gradeLetter so it will be, ‘B’, ‘C’, ‘D’, and ‘E’ successively on subsequent iterations of the outer loop. Next you have the inner for each loop that iterates over each of the names in the current grade array in turn. The output statement uses the Console::Write() funbction so all the names appear on the same line. The names are presented right-justified in the output in a field width of 12, so the names in the lines of output are aligned. After the loop, the WriteLine() just writes a newline to the output so the next grade output starts on a new line. You could have used a for loop for the inner loop: for (int i = 0 ; i < grade->Length ; i++) Console::Write(“{0,12}”,grade[i]);
// Output the current name
The loop is constrained by the Length property of the current array of names that is referenced by the grade variable.
215
Chapter 4 You could also have used a for loop for the outer loop as well, in which case the inner loop needs to be changed further and the nested loop looks like this: for (int j = 0 ; j < grades->Length ; j++) { Console::WriteLine(“Students with Grade {0}:”, gradeLetter+j); for (int i = 0 ; i < grades[j]->Length ; i++) Console::Write(“{0,12}”,grades[j][i]); // Output the current name Console::WriteLine(); }
Now grades[j] references the jth array of names so the expression grades[j][i] references the ith name in the jth array of names.
Strings You have already seen that the String class type that is defined in the System namespace represents a string in C++/CLI(in fact a string consists of Unicode characters. To be more precise it represents a string consisting of a sequence of characters of type System::Char. You get a huge amount of powerful functionality with String class objects so it makes string processing very easy. Let’s start at the beginning with string creation You can create a String object like this: System::String^ saying = L”Many hands make light work.”;
The variable, saying, is a tracking handle that references the String object that is initialized with the string that appears on the right of the =. You must always use a tracking handle to store a reference to a String object. The string literal here is a wide character string because it has the prefix L. If you omit the L prefix, you have a string literal containing 8-bit characters, but the compiler ensures it is converted to a wide-character string. You can access individual characters in a string by using a subscript just like an array, and the first character in the string has an index value of 0. Here’s how you could output the third character in the string saying: Console::WriteLine(“The third character in the string is {0}”, saying[2]);
Note that you can only retrieve a character from a string using an index value; you cannot update the string in this way. String objects are immutable and therefore cannot be modified. You can obtain the number of characters in a string by accessing its Length property. You could output the length of saying with this statement: Console::WriteLine(“The string has {0} characters.”, saying->Length);
Because saying is a tracking handle(which as you know is a kind of pointer(you must use the -> operator to access the Length property (or any other member of the object). You’ll learn more about properties when we get to investigate C++/CLI classes in detail.
216
Arrays, Strings, and Pointers Joining Strings You can use the + operator to join strings to form a new String object. Here’s an example: String^ name1 = L”Beth”; String^ name2 = L”Betty”; String^ name3 = name1 + L” and “ + name2;
After executing these statements, name3 contains the string “Beth and Betty”. Note how you can use the + operator to join String objects with string literals. You can also join String objects with numerical values or bool values and have the values converted automatically to a string before the join operation. The following statements illustrate this: String^ String^ String^ String^
str = L”Value: “; str1 = str + 2.5; str2 = str + 25; str3 = str + true;
// Result is new string “Value: 2.5” // Result is new string “Value: 25” // Result is new string “Value: True”
You can also join a String and a character, but the result depends on the type of character: char ch wchar_t String^ String^
= ‘Z’; wch = ‘Z’ str4 = str + ch; str5 = str + wch;
// Result is new string “Value: 90” // Result is new string “Value: Z”
The comments show the results of the operations. A character of type char is treated as a numerical value so you get the character code value joined to the string. The wchar_t character is of the same type as the characters in the String object (type Char) so the character is appended to the string. Don’t forget that String objects are immutable; once created, they cannot be changed. This means that all operations that apparently modify String objects always result in new String objects being created. The String class also defines a Join() function that you use when you want to join a series of strings stored in an array into a single string with separators between the original strings. Here’s how you could join names together in a single string with the names separated by commas: array<String^>^ names = { “Jill”, “Ted”, “Mary”, “Eve”, “Bill”}; String^ separator = “, “; String^ joined = String::Join(separator, names);
After executing these statements, joined references the string “Jill, Ted, Mary, Eve, Bill”. The separator string has been inserted between each of the original strings in the names array. Of course, the separator string can be anything you like(it could be “ and “, for example, which results in the string “Jill and Ted and Mary and Eve and Bill”. Let’s try a full example of working with String objects.
217
Chapter 4 Try It Out
Working with Strings
Suppose you have an array of integer values that you want to output aligned in columns. You want the values aligned but you want the columns to be just sufficiently wide to accommodate the largest value in the array with a space between columns. This program does that. // Ex4_17.cpp : main project file. // Creating a custom format string #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { array^ values = { 2, 456, 23, -46, 34211, 456, 5609, 112098, 234, -76504, 341, 6788, -909121, 99, 10}; String^ formatStr1 = “{0,”; // 1st half of format string String^ formatStr2 = “}”; // 2nd half of format string String^ number; // Stores a number as a string // Find the length of the maximum length value string int maxLength = 0; // Holds the maximum length found for each(int value in values) { number = “” + value; // Create string from value if(maxLengthLength) maxLength = number->Length; } // Create the format string to be used for output String^ format = formatStr1 + (maxLength+1) + formatStr2; // Output the values int numberPerLine = 3; for(int i = 0 ; i< values->Length ; i++) { Console::Write(format, values[i]); if((i+1)%numberPerLine == 0) Console::WriteLine(); } return 0; }
The output from this program is: 2 456 23 -46 34211 456 5609 112098 234 -76504 341 6788 -909121 99 10 Press any key to continue . . .
218
Arrays, Strings, and Pointers How It Works The objective of this program is to create a format string to align the output of integers from the values array in columns with a width sufficient to accommodate the maximum length string representation of the integers. You create the format string initially in two parts: String^ formatStr1 = “{0,”; String^ formatStr2 = “}”;
// 1st half of format string // 2nd half of format string
These two strings are the beginning and end of the format string you ultimately require. You need to work out the length of the maximum length number string, and sandwich that value between formatStr1 and formatStr2 to form the complete format string. You find the length you require with the following code: int maxLength = 0; for each(int value in values) { number = “” + value; if(maxLengthLength) maxLength = number->Length; }
// Holds the maximum length found
// Create string from value
Within the loop you convert each number from the array to its String representation by joining it to an empty string. You compare the Length property of each string to maxLength, and if it’s greater than the current value of maxLength, it becomes the new maximum length. Creating the format string is simple: String^ format = formatStr1 + (maxLength+1) + formatStr2;
You need to add 1 to maxLength to allow one additional space in the field when the maximum length string is displayed. Placing the expression maxLength+1 between parentheses ensures it is evaluated as an arithmetic operation before the string joining operations are executed. Finally you use the format string in the code to output values from the array: int numberPerLine = 3; for(int i = 0 ; i< values->Length ; i++) { Console::Write(format, values[i]); if((i+1)%numberPerLine == 0) Console::WriteLine(); }
The output statement in the loop uses format as the string for output. With the maxLength plugged into the format string, the output is in columns that are one greater than the maximum length output value. The numberPerLine variable determines how many values appear on a line so the loop is quite general in that you can vary the number of columns by changing the value of numberPerLine.
219
Chapter 4 Modifying Strings The most common requirement for trimming a string is to trim spaces from both the beginning and the end. The trim() function for a string object does that: String^ str = {“ Handsome is as handsome does... String^ newStr = str->Trim();
“};
The Trim() function in the second statement removes any spaces from the beginning and end of str and returns the result as a new String object stored in newStr. Of course, if you did not want to retain the original string, you could store the result back in str. There’s another version of the Trim() function that allows you to specify the characters that are to be removed from the start and end of the string. This function is very flexible because you have more than one way of specifying the characters to be removed. You can specify the characters in an array and pass the array handle as the argument to the function: String^ toBeTrimmed = L”wool wool sheep sheep wool wool wool”; array<wchar_t>^ notWanted = {L’w’,L’o’,L’l’,L’ ‘}; Console::WriteLine(toBeTrimmed->Trim(notWanted));
Here you have a string, toBeTrimmed, that consists of sheep covered in wool. The array of characters to be trimmed from the string is defined by the notWanted array so passing that to the Trim() function for the string removes any of the characters in the array from both ends of the string. Remember, String objects are immutable so the original string is be changed in any way(a new string is created and returned by the Trim() operation. Executing this code fragment produces the output: sheep sheep
If you happen to specify the character literals without the L prefix, they are of type char (which corresponds to the SByte value class type); however, the compiler arranges that they are converted to type wchar_t. You can also specify the characters that the Trim() function is to remove explicitly as arguments so you could write the last line of the previous fragment as: Console::WriteLine(toBeTrimmed->Trim(L’w’, L’o’, L’l’, L’ ‘));
This produces the same output as the previous version of the statement. You can have as many arguments of type wchar_t as you like, but if there are a lot of characters to be specified an array is the best approach. If you want to trim only one end of a string, you can use the TrimEnd() or TrimStart() functions. These come in the same variety of versions as the Trim() function so without arguments you trim spaces, with an array argument you trim the characters in the array, and with explicit wchar_t arguments those characters are removed. The inverse of trimming a string is padding it at either end with spaces or other characters. You have PadLeft() and PadRight() functions that pad a string at the left or right end respectively. The primary use for these functions is in formatting output where you want to place strings either left- or right-justified
220
Arrays, Strings, and Pointers in a fixed width field. The simpler versions of the PadLeft() and PadRight() functions accept a single argument specifying the length of the string that is to result from the operation. For example: String^ value = L”3.142”; String^ leftPadded = value->PadLeft(10); String^ rightPadded = value->PadRight(10);
// Result is “ 3.142” // Result is “3.142 “
If the length you specify as the argument is less than or equal to the length of the original string, either function returns a new String object that is identical to the original. To pad a string with a character other than a space, you specify the padding character as the second argument to the PadLeft() or PadRight() functions. Here are a couple of examples of this: String^ value = L”3.142”; String^ leftPadded = value->PadLeft(10, L’*’); // Result is “*****3.142” String^ rightPadded = value->PadRight(10, L’#’); // Result is “3.142#####”
Of course, with all these examples, you could store the result back in the handle referencing the original string, which would discard the original string. The String class also has the ToUpper() and ToLower() functions to convert an entire string to upperor lowercase. Here’s how that works: String^ proverb = L”Many hands make light work.”; String^ upper = proverb->ToUpper(); // Result “MANY HANDS MAKE LIGHT WORK.”
The ToUpper() function returns a new string that is the original string converted to uppercase. You use the Insert() function to insert a string at a given position in an existing string. Here’s an example of doing that: String^ proverb = L”Many hands make light work.”; String^ newProverb = proverb->Insert(5, L”deck “);
The function inserts the string specified by the second argument starting at the index position in the old string specified by the first argument. The result of this operation is a new string containing: Many deck hands make light work.
You can also replace all occurrences of a given character in a string with another character or all occurrences of a given substring with another substring. Here’s a fragment that shows both possibilities: String^ proverb = L”Many hands make light work.”; Console::WriteLine(proverb->Replace(L’ ‘, L’*’); Console::WriteLine(proverb->Replace(L”Many hands”, L”Pressing switch”);
Executing this code fragment produces the output: Many*hands*make*light*work. Pressing switch make light work.
The first argument to the Replace() function specifies the character or substring to be replaced and the second argument specifies the replacement.
221
Chapter 4 Searching Strings Perhaps the simplest search operation is to test whether a string starts or ends with a given substring. The StartsWith() and EndsWith() functions do that. You supply a handle to the substring you are looking for as the argument to either function, and the function returns a bool value that indicates whether or not the substring is present. Here’s a fragment showing how you might use the StartsWith() function: String^ sentence = L”Hide, the cow’s outside.”; if(sentence->StartsWith(L”Hide”)) Console::WriteLine(“The sentence starts with ‘Hide’.”);
Executing this fragment results in the output: The sentence starts with ‘Hide’.
Of course, you could also apply the EndsWith() function to the sentence string: Console::WriteLine(“The sentence does{0] end with ‘outside’.”, sentence->EndsWith(L”outside”) ? L”” : L” not”);
The result of the conditional operator expression is inserted in the output string. This is an empty string if EndsWith() returns true and “ not” if it returns false. In this instance the function returns false (because of the period at the end of the sentence string). The IndexOf() function searches a string for the first occurrence of a specified character or a substring and returns the index if it is present or -1 if it is not found. You specify the character or the substring you are looking for as the argument to the function. For example: String^ sentence = L”Hide, the cow’s outside.”; int ePosition = sentence->IndexOf(L’e’); int thePosition = sentence->IndexOf(L”the”);
// Returns 3 // Returns 6
The first search is for the letter ‘e’ and the second is for the word “the”. The values returned by the IndexOf() function are indicated in the comments. More typically you will want to find all occurrences of a given character or substring and another version of the IndexOf() function is designed to be used repeatedly to enable you to do that. In this case you supply a second argument specifying the index position where the search is to start. Here’s an example of how you might use the function in this way: int index = 0; int count = 0; while((index = words->IndexOf(word,index)) >= 0) { index += word->Length; ++count; } Console::WriteLine(L”’{0}’ was found {1} times in:\n{2}”, word, count, words);
This fragment counts the number of occurrences of “wool” on the words string. The search operation appears in the while loop condition and the result is stored in index. The loop continues as long as
222
Arrays, Strings, and Pointers index is non-negative so when IndexOf() returns -1 the loop ends. Within the loop body, the value of index is incremented by the length of word, which moves the index position to the character following the instance of word that was found, ready for the search on the next iteration. The count variable is
incremented within the loop so when the loop ends it has accumulated the total number of occurrences of word in words. Executing the fragment results in the following output: ‘wool’ was found 5 times in: wool wool sheep sheep wool wool wool
The LastIndexOf() function is similar to the IndexOf() functions except that is searches backwards through the string from the end or from a specified index position. Here’s how the operation performed by the previous fragment could be performed using the LastIndexOf() function: int index = words->Length - 1; int count = 0; while(index >= 0 && (index = words->LastIndexOf(word,index)) >= 0) { --index; ++count; }
With the word and words string the same as before, this fragment produces the same output. Because LastIndexOf() searches backwards, the starting index is the last character in the string, which is words->Length-1. When an occurrence of word is found, you must now decrement index by 1 so that the next backward search starts at the character preceding the current occurrence of word. If word occurs right at the beginning of words(at index position 0 — decrementing index results in –1, which is not a legal argument to the LastIndexOf() function because the search starting position must always be within the string. The addition check for a negative value of index in the loop condition prevents this from happening; if the right operand of the && operator is false, the left operand is not evaluated. The last search function I want to mention is IndexOfAny() that searches a string for the first occurrence of any character in the array of type array<wchar_t> that you supply as the argument. Similar to the IndexOf() function, the IndexOfAny() function comes in versions that searches from the beginning of a string or from a specified index position. Let’s try a full working example of using the IndexOfAny() function.
Try It Out
Searching for any of a Set of Characters
This example searches a string for punctuation characters: // Ex4_18.cpp : main project file. // Searching for punctuation #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { array<wchar_t>^ punctuation = {L’”’, L’\’’, L’.’, L’,’, L’:’, L’;’, L’!’, L’?’}; String^ sentence = L”\”It’s chilly in here\”, the boy’s mother said coldly.”; // Create array of space characters same length as sentence
223
Chapter 4 array<wchar_t>^ indicators = gcnew array<wchar_t>(sentence->Length){L’ ‘}; int index = 0; // Index of character found int count = 0; // Count of punctuation characters while((index = sentence->IndexOfAny(punctuation, index)) >= 0) { indicators[index] = L’^’; // Set marker ++index; // Increment to next character ++count; // Increase the count } Console::WriteLine(L”There are {0} punctuation characters in the string:”, count); Console::WriteLine(L”\n{0}\n{1}”, sentence, gcnew String(indicators)); return 0; }
This example should produce the following output: There are 6 punctuation characters in the string: “It’s chilly in here”, the boy’s mother said coldly. ^ ^ ^^ ^ ^ Press any key to continue . . .
How It Works You first create an array containing the characters to be found and the string to be searched: array<wchar_t>^ punctuation = {L’”’, L’\’’, L’.’, L’,’, L’:’, L’;’, L’!’, L’?’}; String^ sentence = L”\”It’s chilly in here\”, the boy’s mother said coldly.”;
Note that you must specify a single quote character using an escape sequence because a single quote is a delimiter in a character literal. You can use a double quote explicitly in a character literal because there’s no risk of it being interpreted as a delimiter in this context. Next you define an array of characters with the elements initialized to a space character: array<wchar_t>^ indicators = gcnew array<wchar_t>(sentence->Length){L’ ‘};
This array has as many elements as the sentence string has characters. You’ll be using this array in the output to mark where punctuation characters occur in the sentence string. You’ll just change the appropriate array element to ‘^’ whenever a punctuation character is found. Note how a single initializer between the braces following the array specification can be used to initialize all the elements in the array. The search takes place in the while loop: while((index = sentence->IndexOfAny(punctuation, index)) >= 0) { indicators[index] = L’^’; // Set marker ++index; // Increment to next character ++count; // Increase the count }
224
Arrays, Strings, and Pointers The loop condition is essentially the same as you have seen in earlier code fragments. Within the loop body you update the indicators array element at position index to be a ‘^’ character before incrementing index ready for the next iteration. When the loop ends, count contains the number of punctuation characters that were found, and indicators will contain ‘^’ characters at the positions in sentence where such characters were found. The output is produced by the statements: Console::WriteLine(L”There are {0} punctuation characters in the string:”, count); Console::WriteLine(L”\n{0}\n{1}” sentence, gcnew String(indicators));
The second statement creates a new String object on the heap from the indicators array by passing the array to the String class constructor. A class constructor is a function that will create a class object when it is called. You’ll learn more about constructors when you get into defining your own classes.
Tracking References A tracking reference provides a similar capability to a native C++ reference in that it represents an alias for something on the CLR heap. You can create tracking references to value types on the stack and to handles in the garbage-collected heap; the tracking references themselves are always created on the stack. A tracking reference is automatically updated if the object referenced is moved by the garbage collector. You define a tracking reference using the % operator. For example, here’s how you could create a tracking reference to a value type: int value = 10; int% trackValue = value;
The second statement defines stackValue to be a tracking reference to the variable value, which has been created on the stack. You can now modify value using stackValue: trackValue *= 5; Console::WriteLine(value);
Because trackValue is an alias for value, the second statement outputs 50.
Interior Pointers Although you cannot perform arithmetic on the address in a tracking handle, C++/CLI does provide a form of pointer with which it is possible to apply arithmetic operations; it’s called an interior pointer and is defined using the keyword interior_ptr. The address stored in an interior pointer can be updated automatically by the CLR garbage collection when necessary. An interior point is always an automatic variable that is local to a function.
225
Chapter 4 Here’s how you could define an interior point containing the address of the first element in an array: array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; interior_ptr<double> pstart = &data[0];
You specify the type of object pointed to by the interior pointer between angled brackets following the interior_ptr keyword. In the second statement here you initialize the pointer with the address of the first element in the array using the & operator, just as you would with a native C++ pointer. If you do not provide an initial value for an interior pointer, it is initialized with nullptr by default. An array is always allocated on the CLR heap so here’s a situation where the garbage collector may adjust the address contained in an interior pointer. There are constraints on the type specification for an interior pointer. An interior pointer can contain the address of a value class object on the stack or the address of a handle to an object on the CLR heap; it cannot contain the address of a whole object on the CLR heap. An interior pointer can also point to a native class object or a native pointer. You can also use an interior pointer to hold the address of a value class object that is part of an object on the heap, such as an element of a CLR array. This you can create an interior pointer that can store the address of a tracking handle to a System::String object but you cannot create an interior pointer to store the address of the String object itself. For example: interior_ptr<String^> pstr1; interior_ptr<String> pstr2;
// OK - pointer to a handle // Will not compile - pointer to a String object
All the arithmetic operations that you can apply to a native C++ pointer you can also apply to an interior pointer. You can use the increment and decrement an interior pointer to the change the address it contains to refer to the following or preceding data item. You can also add or subtract integer values and compare interior points. Let’s put together an example that does some of that.
Try It Out
Creating and Using Interior Pointers
This example exercises interior pointers with numerical values and strings: // Ex4_19.cpp : main project file. // Creating and using interior pointers #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { // Access array elements through a pointer array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; interior_ptr<double> pstart = &data[0]; interior_ptr<double> pend = &data[data->Length - 1]; double sum = 0.0; while(pstart <= pend)
226
Arrays, Strings, and Pointers sum += *pstart++; Console::WriteLine(L”Total of data array elements = {0}\n”, sum); // Just to show we can - access strings through an interior pointer array<String^>^ strings = { L”Land ahoy!”, L”Splice the mainbrace!”, L”Shiver me timbers!”, L”Never throw into the wind!” }; for(interior_ptr<String^> pstrings = &strings[0] ; pstrings-&strings[0] < strings->Length ; ++pstrings) Console::WriteLine(*pstrings); return 0; }
The output from this example is: Total of data array elements = 18 Land ahoy! Splice the mainbrace! Shiver me timbers! Never throw into the wind! Press any key to continue . . .
How It Works After creating the data array of elements of type double, you define two interior pointers: interior_ptr<double> pstart = &data[0]; interior_ptr<double> pend = &data[data->Length - 1];
The first statement creates pstart as a pointer to type double and initializes it with the address of the first element in the array, data[0]. The interior pointer, pend, is initialized with the address of the last element in the array, data[data->Length - 1]. Because data->Length is the number of elements in the array, subtracting 1 from this value produces the index for the last element. The while loop accumulates the sum of the elements in the array: while(pstart <= pend) sum += *pstart++;
The loop continues as long as the interior pointer, pstart, contains an address that is not greater than the address in pend. You could equally well have expressed the loop condition as !pstart > pend. Within the loop pstart starts out containing the address of the first array element. The value of the first element is obtained by dereferencing the pointer with the expression *pstart and the result of this is added to sum. The address in the pointer is then incremented using the ++ operator. On the last loop iteration, pstart contains the address of the last element which is the same as the address value that pend
227
Chapter 4 contains, so incrementing pstart makes the loop condition false because pstart is then greater than pend. After the loop ends the value of sum is written out so you can confirm that the while loop is working as it should. Next you create an array of four strings: array<String^>^ strings = { L”Land ahoy!”, L”Splice the mainbrace!”, L”Shiver me timbers!”, L”Never throw into the wind!” };
The for loop then outputs each string to the command line: for(interior_ptr<String^> pstrings = &strings[0] ; pstrings-&strings[0] < strings->Length ; ++pstrings) Console::WriteLine(*pstrings);
The first expression in the for loop condition declares the interior pointer, pstrings, and initializes it with the address of the first element in the strings array. The second expression that determines whether the for loop continues is: pstrings-&strings[0] < strings->Length
As long as pstrings contains the address of a valid array element, the difference between the address in pstrings and the address of the first element in the array is less than the number of elements in the array, given by the expression strings->Length. Thus when this difference equals the length of the array, the loop ends. You can see from the output that everything works as expected. The most frequent use of an interior pointer is to reference objects that are part of a CLR heap object and you’ll see more about this later in the book.
Summar y You are now familiar with all of the basic types of values in C++, how to create and use arrays of those types, and how to create and use pointers. You have also been introduced to the idea of a reference. However, we have not exhausted all of these topics. I’ll come back to the topics of arrays, pointers, and references later in the book. The important points discussed in this chapter relating to native C++ programming are:
228
❑
An array allows you to manage a number of variables of the same type using a single name. Each dimension of an array is defined between square brackets following the array name in the declaration of the array.
❑
Each dimension of an array is indexed starting from zero. Thus the fifth element of a onedimensional array has the index value 4.
❑
Arrays can be initialized by placing the initializing values between curly braces in the declaration.
Arrays, Strings, and Pointers ❑
A pointer is a variable that contains the address of another variable. A pointer is declared as a ‘pointer to type’ and may only be assigned addresses of variables of the given type.
❑
A pointer can point to a constant object. Such a pointer can be reassigned to another object. A pointer may also be defined as const, in which case it can’t be reassigned.
❑
A reference is an alias for another variable, and can be used in the same places as the variable it references. A reference must be initialized in its declaration.
❑
A reference can’t be reassigned to another variable.
❑
The operator sizeof returns the number of bytes occupied by the object specified as its argument. Its argument may be a variable or a type name between parentheses.
❑
The operator new allocates memory dynamically in the free store in a native C++ application. When memory has been assigned as requested, it returns a pointer to the beginning of the memory area provided. If memory cannot be assigned for any reason, an exception is thrown that by default causes the program to terminate.
The pointer mechanism is sometimes a bit confusing because it can operate at different levels within the same program. Sometimes it is operating as an address, and at other times it can be operating with the value stored at an address. It’s very important that you feel at ease with the way pointers are used, so if you find that they are in any way unclear, try them out with a few examples of your own until you feel confident about applying them. The key points that you learned about in relation to programming for the CLR are: ❑
In CLR program, you allocate memory of the garbage-collected heap using the gcnew operator.
❑
Reference class objects in general and String objects in particular are always allocated on the CLR heap.
❑
You use String objects when working with strings in a CLR program.
❑
The CLR has its own array types with more functionality that native array types.
❑
CLR arrays are created on the CLR heap.
❑
A tracking handle is a form of pointer used to reference variables defined on the CLR heap. A tracking handle is automatically updated if what it refers to is relocated in the heap by the garbage collector.
❑
Variable that reference objects and arrays on the heap are always tracking handles.
❑
A tracking reference is similar to a native reference except that the address it contains is automatically updated if the object referenced is moved by the garbage collector.
❑
An interior pointer is a C++/CLI pointer type to which you can apply the same operation as a native pointer.
❑
The address contained in interior pointer can be modified using arithmetic operations and still maintain an address correctly even when referring to something stored in the CLR heap.
229
Chapter 4
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
Write a native C++ program that allows an unlimited number of values to be entered and stored in an array allocated in the free store. The program should then output the values five to a line followed by the average of the values entered. The initial array size should be five elements. The program should create a new array with five additional elements when necessary and copy values from the old array to the new.
2. 3.
Repeat the previous exercise but use pointer notation throughout instead of arrays. Declare a character array, and initialize it to a suitable string. Use a loop to change every other character to upper case. Hint: In the ASCII character set, values for uppercase characters are 32 less than their lowercase counterparts.
4.
Write a C++/CLI program that creates an array with a random number of elements of type int. The array should have from 10 to 20 elements. Set the array elements to random values between 100 and 1000. Output the elements five to a line in ascending sequence without sorting the array; for example find the smallest element and output that, then the next smallest, and so on.
5.
Write a C++/CLI program that will generate a random integer greater than 10,000. Output the integer and then output the digits in the integer in words. For example, if the integer generate was 345678, then the output should be:
The value is 345678 three four five six seven eight
6.
Write a C++/CLI program that creates an array containing the following strings:
“Madam I’m Adam.” “Don’t cry for me, Marge and Tina.” “Lid off a daffodil.” “Red lost soldier.” “Cigar? Toss it in a can. It is so tragic.”
The program should examine each string in turn, output the string and indicate whether it is or is not a palindrome (that is, a sequence of letters reading backwards or forwards, ignoring spaces and punctuation).
230
5 Introducing Structure into Your Programs Up to now, you haven’t really been able to structure your program code in a modular fashion because you have only been able to construct a program as a single function, main(); but you have been using library functions of various kinds as well as functions belonging to objects. Whenever you write a C++ program, you should have a modular structure in mind from the outset and, as you’ll see, a good understanding of how to implement functions is essential to object-oriented programming in C++. In this chapter, you’ll learn: ❑
How to declare and write your own C++ functions
❑
How function arguments are defined and used
❑
How arrays can be passed to and from a function
❑
What pass-by-value means
❑
How to pass pointers to functions
❑
How to use references as function arguments, and what pass-by-reference means
❑
How the const modifier affects function arguments
❑
How to return values from a function
❑
How recursion can be used
There’s quite a lot to structuring your C++ programs, so to avoid indigestion, you won’t try to swallow the whole thing in one gulp. After you have chewed over and gotten the full flavor of these morsels, you’ll move on to the next chapter, where you will get further into the meat of the topic.
Chapter 5
Understanding Functions First take a look at the broad principles of how a function works. A function is a self-contained block of code with a specific purpose. A function has a name that both identifies it and is used to call it for execution in a program. The name of a function is global but is not necessarily unique in C++, as you’ll see in the next chapter; however, functions that perform different actions should generally have different names. The name of a function is governed by the same rules as those for a variable. A function name is, therefore, a sequence of letters and digits, the first of which is a letter, where an underscore counts as a letter. The name of a function should generally reflect what it does, so for example, you might call a function that counts beans count_beans(). You pass information to a function by means of arguments specified when you invoke it. These arguments need to correspond with parameters that appear in the definition of the function. The arguments that you specify replace the parameters used in the definition of the function when the function executes. The code in the function then executes as though it was written using your argument values. Figure 5-1 illustrates the relationship between arguments in the function call and the parameters specified in the definition of the function. Arguments
cout << add_ints( 2 , 3 );
Argument values replace corresponding parameters in the function definition
Function Definition
Value 5 returned
Figure 5-1
232
int add_ints( int i, int j ) { return i + j ; }
Parameters
Introducing Structure into Your Programs In this example, the function returns the sum of the two arguments passed to it. In general, a function returns either a single value to the point in the program where it was called, or nothing at all, depending on how the function is defined. You might think that returning a single value from a function is a constraint, but the single value returned can be a pointer that might contain the address of an array, for example. You will see more about how data is returned from a function a little later in this chapter.
Why Do You Need Functions? One major advantage that a function offers is that it can be executed as many times as necessary from different points in a program. Without the ability to package a block of code into a function, programs would end up being much larger because you would typically need to replicate the same code at various points in them. But the real reason that you need functions is to break up a program into easily manageable chunks for development and testing. Imagine a really big program — let’s say a million lines of code. A program of this size would be virtually impossible to write without functions. Functions enable you to segment a program so that you can write the code piecemeal, and test each piece independently before bringing it together with the other pieces. It also allows the work to be divided among members of a programming team, with each team member taking responsibility for a tightly specified piece of the program, with a well-defined functional interface to the rest of the code.
Structure of a Function As you have seen when writing the function main(), a function consists of a function header that identifies the function, followed by the body of the function between curly braces containing the executable code for the function. Let’s look at an example. You could write a function to raise a value to a given power, that is, to compute the result of multiplying the value x by itself n times, which is xn: // Function to calculate x to the power n, with n greater than or equal to 0 double power(double x, int n) // Function header { // Function body starts here... double result = 1.0; // Result stored here for(int i = 1; i <= n; i++) result *= x; return result; }
// ...and ends here
The Function Header Let’s first examine the function header in this example. The following is the first line of the function. double power(double x, int n)
// Function header
It consists of three parts: ❑
The type of the return value (double in this case)
❑
The name of the function, (power in this case)
❑
The parameters of the function enclosed between parentheses (x and n in this case, of types double and int respectively)
233
Chapter 5 The return value is returned to the calling function when the function is executed, so when the function is called, it results in a value of type double in the expression in which it appears. Our function has two parameters: x, the value to be raised to a given power which is of type double, and the value of the power, n, which is of type int. The computation that the function performs is written using these parameter variables together with another variable, result, declared in the body of the function. The parameter names and any variables defined in the body of the function are local to the function. Note that no semicolon is required at the end of the function header or after the closing brace for the function body.
The General Form of a Function Header The general form of a function header can be written as follows. return_type function_name(parameter_list)
The return_type can be any legal type. If the function does not return a value, the return type is specified by the keyword void. The keyword void is also used to indicate the absence of parameters, so a function that has no parameters and doesn’t return a value would have the following function header. void my_function(void)
An empty parameter list also indicates that a function takes no arguments, so you could omit the keyword void between the parentheses like: void my_function()
A function with a return type specified as void should not be used in an expression in the calling program. Because it doesn’t return a value, it can’t sensibly be part of an expression, so using it in this way causes the compiler to generate an error message.
The Function Body The desired computation in a function is performed by the statements in the function body following the function header. The first of these in our example declares a variable result that is initialized with the value 1.0. The variable result is local to the function, as are all automatic variables declared within the function body. This means that the variable result ceases to exist after the function has completed execution. What might immediately strike you is that if result ceases to exist on completing execution of the function, how is it returned? The answer is that a copy of the value being returned is made automatically, and this copy is available to the return point in the program. The calculation is performed in the for loop. A loop control variable i is declared in the for loop which assumes successive values from 1 to n. The variable result is multiplied by x once for each loop iteration, so this occurs n times to generate the required value. If n is 0, the statement in the loop won’t be executed at all because the loop continuation condition immediately fails, and so result is left as 1.0. As I’ve said, the parameters and all the variables declared within the body of a function are local to the function. There is nothing to prevent you from using the same names for variables in other functions for
234
Introducing Structure into Your Programs quite different purposes. Indeed, it’s just as well this is so because it would be extremely difficult to ensure variables names were always unique within a program containing a large number of functions, particularly if the functions were not all written by the same person. The scope of variables declared within a function is determined in the same way that I have already discussed. A variable is created at the point at which it is defined and ceases to exist at the end of the block containing it. There is one type of variable that is an exception to this(variables declared as static. I’ll discuss static variables a little later in this chapter. Be careful about masking global variables with local variables of the same name. You first met this situation back in Chapter 2 where you saw how you could use the scope resolution operator :: to access global variables.
The return Statement The return statement returns the value of result to the point where the function was called. The general form of the return statement is: return expression;
where expression must evaluate to a value of the type specified in the function header for the return value. The expression can be any expression you want, as long as you end up with a value of the required type. It can include function calls — even a call of the same function in which it appears, as you’ll see later in this chapter. If the type of return value has been specified as void, there must be no expression appearing in the return statement. It must be written simply as: return;
Using a Function At the point at which you use a function in a program, the compiler must know something about it to compile the function call. It needs enough information to be able to identify the function, and to verify that you are using it correctly. Unless you the definition of the function that you intend to use appears earlier in the same source file, you must declare the function using a statement called a function prototype.
Function Prototypes A prototype of a function provides the basic information that the compiler needs to check that you are using a function correctly. It specifies the parameters to be passed to the function, the function name, and the type of the return value — basically, it contains the same information as appears in the function header, with the addition of a semicolon. Clearly, the number of parameters and their types must be the same in the function prototype as they are in the function header in the definition of the function. The prototypes for the functions that you call from within another function must appear before the statements doing the calling and are usually placed at the beginning of the program source file. The header files that you’ve been including for standard library functions contain the prototypes of the functions provided by the library, amongst other things.
235
Chapter 5 For the power() function example, you could write the prototype as: double power(double value, int index);
Don’t forget that a semicolon is required at the end of a function prototype. Without it, you get error messages from the compiler. Note that I have specified names for the parameters in the function prototype that are different from those I used in the function header when I defined the function. This is just to indicate that it’s possible. Most often, the same names are used in the prototype and in the function header in the definition of the function, but this doesn’t have to be so. You can use longer more expressive parameter names in the function prototype to aid understanding of the significance of the parameters and then use shorter parameter names in the function definition where the longer names would make the code in the body of the function less readable. If you like, you can even omit the names altogether in the prototype, and just write: double power(double, int);
This provides enough information for the compiler to do its job; however, it’s better practice to use some meaningful name in a prototype because it aids readability and, in some cases, makes all the difference between clear code and confusing code. If you have a function with two parameters of the same type (suppose our index was also of type double in the function power(), for example), the use of suitable names indicates which parameter appears first and which second.
Try It Out
Using a Function
You can see how all this goes together in an example exercising the power() function. // Ex5_01.cpp // Declaring, defining, and using a function #include using std::cout; using std::endl; double power(double x, int n); int main(void) { int index = 3; double x = 3.0; double y = 0.0; y = power(5.0, 3); cout << endl << “5.0 cubed = “ << y; cout << endl << “3.0 cubed = “ << power(3.0, index);
// Function prototype
// Raise to this power // Different x from that in function power
// Passing constants as arguments
// Outputting return value
x = power(x, power(2.0, 2.0)); // Using a function as an argument
236
Introducing Structure into Your Programs cout << endl << “x = “ << x;
// with auto conversion of 2nd parameter
cout << endl; return 0; } // Function to compute positive integral powers of a double value // First argument is value, second argument is power index double power(double x, int n) { // Function body starts here... double result = 1.0; // Result stored here for(int i = 1; i <= n; i++) result *= x; return result; } // ...and ends here
This program shows some of the ways in which you can use the function power(), specifying the arguments to the function in a variety of ways. If you run this example, you get the following output: 5.0 cubed = 125 3.0 cubed = 27 x = 81
How It Works After the usual #include statement for input/output and the using declarations, you have the prototype for the function power(). If you were to delete this and try recompiling the program, the compiler wouldn’t be able to process the calls to the function in main() and would instead generate a whole series of error messages: error C3861: ‘power’: identifier not found
and the error message: error C2365: ‘power’ : redefinition; previous definition was ‘formerly unknown identifier’
In a change from previous examples, I’ve used the new keyword void in the function main() where the parameter list would usually appear to indicate that no parameters are to be supplied. Previously, I left the parentheses enclosing the parameter list empty, which is also interpreted in C++ as indicating that there are no parameters; but it’s better to specify the fact by using the keyword void. As you saw, the keyword void can also be used as the return type for a function to indicate that no value is returned. If you specify the return type of a function as void, you must not place a value in any return statement within the function; otherwise, you get an error message from the compiler. You gathered from some of the previous examples that using a function is very simple. To use the function power() to calculate 5.03 and store the result in a variable y in our example, you have the following statement: y = power(5.0, 3);
237
Chapter 5 The values 5.0 and 3 here are the arguments to the function They happen to be constants, but you can use any expression as an argument, as long as a value of the correct type is ultimately produced. The arguments to the power() function substitute for the parameters x and n, which were used in the definition of the function. The computation is performed using these values and then a copy of the result, 125, is returned to the calling function, main(), which is then stored in y. You can think of the function as having this value in the statement or expression in which it appears. You then output the value of y: cout << endl << “5.0 cubed = “ << y;
The next call of the function is used within the output statement: cout << endl << “3.0 cubed = “ << power(3.0, index);
// Outputting return value
Here, the value returned by the function is transferred directly to the output stream. Because you haven’t stored the returned value anywhere, it is otherwise unavailable to you. The first argument in the call of the function here is a constant; the second argument is a variable. The function power() is used next in this statement: x = power(x, power(2.0, 2.0));
// Using a function as an argument
Here the power() function is called twice. The first call to the function is the rightmost in the expression, and the result supplies the value for the second argument to the leftmost call. Although the arguments in the sub-expression power(2.0, 2.0) are both specified as the double literal 2.0, the function is actually called with the first argument as 2.0 and the second argument as the integer literal, 2. The compiler converts the double value specified for the second argument to type int because it knows from the function prototype (shown again below) that the type of the second parameter has been specified as int. double power(double x, int n);
// Function prototype
The double result 4.0 is returned by the first call to the power() function, and after conversion to type int, the value 4 is passed as the second argument in the next call of the function, with x as the first argument. Because x has the value 3.0, the value of 3.04 is computed and the result, 81.0, stored in x. This sequence of events is illustrated in Figure 5-2. This statement involves two implicit conversions from type double to type int that were inserted by the compiler. There’s a possible loss of data when converting from type double to type int so the compiler issues warning message when this occurs, even though the compiler has itself inserted this conversion. Generally relying on automatic conversions where there is potential for data loss is a dangerous programming practice, and it is not at all obvious from the code that this conversion is intended. It is far better to be explicit in your code by using the static_cast operator when necessary. The statement in the example is much better written as: x = power(x, static_cast(power(2.0, 2)));
238
Introducing Structure into Your Programs x = power( x , power( 2.0 , 2.0 ));
initial value 3.0
1 Converted
to type int
5 result stored
back in x
2
power( 2.0 , 2 ) 4.0 (type double)
Converted 3 to type int
4
power( 3.0 , 4 ) 81.0 (type double)
Figure 5-2
Coding the statement like this avoids both the compiler warning messages that the original version caused. Using a static cast does not remove the possibility of losing data in the conversion of data from one type to another. Because you specified it though, it is clear that this is what you intended, recognizing that data loss might occur.
Passing Arguments to a Function It’s very important to understand how arguments are passed to a function, as it affects how you write functions and how they ultimately operate. There are also a number of pitfalls to be avoided, so we’ll look at the mechanism for this quite closely. The arguments you specify when a function is called should usually correspond in type and sequence to the parameters appearing in the definition of the function. As you saw in the last example, if the type of an argument specified in a function call doesn’t correspond with the type of parameter in the function definition, (where possible) it converts to the required type, obeying the same rules as those for casting operands that were discussed in Chapter 2. If this proves not to be possible, you get an error message from the compiler; however, even if the conversion is possible and the code compiles, it could well result in the loss of data (for example from type long to type short) and should therefore be avoided. There are two mechanisms used generally in C++ to pass arguments to functions. The first mechanism applies when you specify the parameters in the function definition as ordinary variables (not references). This is called the pass-by-value method of transferring data to a function so let’s look into that first of all.
239
Chapter 5
The Pass-by-value Mechanism With this mechanism, the variables or constants that you specify as arguments are not passed to a function at all. Instead, copies of the arguments are created and these copies are used as the values to be transferred. Figure 5-3 shows this in a diagram using the example of our power() function. int index = 2; double value = 10.0; double result = power(value, index);
index
2 Temporary copies of the arguments are made for use in the function
value
10.0
copy of value
copy of index 2
10.0
double power ( double x { ...
,
int n )
The original arguments are not accessible here, only the copies.
}
Figure 5-3
Each time you call the function power(), the compiler arranges for copies of the arguments that you specify to be stored in a temporary location in memory. During execution of the functions, all references to the function parameters are mapped to these temporary copies of the arguments.
Try It Out
Passing-by-value
One consequence of the pass-by-value mechanism is that a function can’t directly modify the arguments passed. You can demonstrate this by deliberately trying to do so in an example: // Ex5_02.cpp // A futile attempt to modify caller arguments #include
240
Introducing Structure into Your Programs using std::cout; using std::endl; int incr10(int num);
// Function prototype
int main(void) { int num = 3; cout << endl << “incr10(num) = “ << incr10(num) << endl << “num = “ << num; cout << endl; return 0; } // Function to increment a variable by 10 int incr10(int num) // Using the same name might help... { num += 10; // Increment the caller argument – hopefully return num;
// Return the incremented value
}
Of course, this program is doomed to failure. If you run it, you get this output: incr10(num) = 13 num = 3
How It Works The output confirms that the original value of num remains untouched. The incrementing occurred on the copy of num that was generated and was eventually discarded on exiting from the function. Clearly, the pass-by-value mechanism provides you with a high degree of protection from having your caller arguments mauled by a rogue function, but it is conceivable that you might actually want to arrange to modify caller arguments. Of course, there is a way to do this. Didn’t you just know that pointers would turn out to be incredibly useful?
Pointers as Arguments to a Function When you use a pointer as an argument, the pass-by-value mechanism still operates as before; however, a pointer is an address of another variable, and if you take a copy of this address, the copy still points to the same variable. This is how specifying a pointer as a parameter enables your function to get at a caller argument.
241
Chapter 5 Try It Out
Pass-by-pointer
You can change the last example to use a pointer to demonstrate the effect: // Ex5_03.cpp // A successful attempt to modify caller arguments #include using std::cout; using std::endl; int incr10(int* num);
// Function prototype
int main(void) { int num = 3; int* pnum = #
// Pointer to num
cout << endl << “Address passed = “ << pnum; cout << endl << “incr10(pnum) = “ << incr10(pnum); cout << endl << “num = “ << num; cout << endl; return 0; } // Function to increment a variable by 10 int incr10(int* num) // Function with pointer argument { cout << endl << “Address received = “ << num; *num += 10; return *num;
// Increment the caller argument // - confidently // Return the incremented value
}
The output from this example is: Address passed = 0012FF6C Address received = 0012FF6C incr10(pnum) = 13 num = 13
The address values produced by your computer may be different from those shown above, but the two values should be identical to each other.
242
Introducing Structure into Your Programs How It Works In this example, the principal alterations from the previous version relate to passing a pointer, pnum, in place of the original variable, num. The prototype for the function now has the parameter type specified as a pointer to int, and the main() function has the pointer pnum declared and initialized with the address of num. The function main(), and the function incr10(), output the address sent and the address received respectively, to verify that the same address is indeed being used in both places. The output shows that this time the variable num has been incremented and has a value that’s now identical to that returned by the function. In the rewritten version of the function incr10(), both the statement incrementing the value passed to the function and the return statement now de-reference the pointer to use the value stored.
Passing Arrays to a Function You can also pass an array to a function, but in this case the array is not copied, even though a pass-byvalue method of passing arguments still applies. The array name is converted to a pointer, and a copy of the pointer to the beginning of the array is passed by value to the function. This is quite advantageous because copying large arrays is very time consuming. As you may have worked out, however, elements of the array may be changed within a function and thus an array is the only type that cannot be passed by value.
Try It Out
Passing Arrays
You can illustrate the ins and outs of this by writing a function to compute the average of a number of values passed to a function in an array. // Ex5_04.cpp // Passing an array to a function #include using std::cout; using std::endl; double average(double array[], int count);
//Function prototype
int main(void) { double values[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 }; cout << endl << “Average = “ << average(values, (sizeof values)/(sizeof values[0])); cout << endl; return 0; } // Function to compute an average double average(double array[], int count) {
243
Chapter 5 double sum = 0.0; for(int i = 0; i < count; i++) sum += array[i];
// Accumulate total in here // Sum array elements
return sum/count;
// Return average
}
The program produces the following output: Average = 5.5
How It Works The average()function is designed to work with an array of any length. As you can see from the prototype, it accepts two arguments: the array and a count of the number of elements. Because you want it to work with arrays of arbitrary length, the array parameter appears without a dimension specified. The function is called in main() in this statement, cout << endl << “Average = “ << average(values, (sizeof values)/(sizeof values[0]));
The function is called with the first argument as the array name, values, and the second argument as an expression that evaluates to the number of elements in the array. You’ll recall this expression, using the operator sizeof, from when we looked at arrays in Chapter 4. Within the body of the function, the computation is expressed in the way you would expect. There’s no significant difference between this and the way you would write the same computation if you implemented it directly in main(). The output confirms that everything works as we anticipated.
Try It Out
Using Pointer Notation When Passing Arrays
You haven’t exhausted all the possibilities here. As we determined at the outset, the array name is passed as a pointer(to be precise, as a copy of a pointer, so within the function you are not obliged to work with the data as an array at all. You could modify the function in the example to work with pointer notation throughout, in spite of the fact that we are using an array. // Ex5_05.cpp // Handling an array in a function as a pointer #include using std::cout; using std::endl; double average(double* array, int count); int main(void) {
244
//Function prototype
Introducing Structure into Your Programs double values[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 }; cout << endl << “Average = “ << average(values, (sizeof values)/(sizeof values[0])); cout << endl; return 0; } // Function to compute an average double average(double* array, int count) { double sum = 0.0; // Accumulate total in here for(int i = 0; i < count; i++) sum += *array++; // Sum array elements return sum/count;
// Return average
}
The output is exactly the same as in the previous example.
How It Works As you can see, the program needed very few changes to make it work with the array as a pointer. The prototype and the function header have been changed, although neither change is absolutely necessary. If you change both back to the original version with the first parameter specified as a double array and leave the function body written in terms of a pointer, it works just as well. The most interesting aspect of this version is the body of the for loop statement: sum += *array++;
// Sum array elements
Here you apparently break the rule about not being able to modify an address specified as an array name because you are incrementing the address stored in array. In fact, you aren’t breaking the rule at all. Remember that the pass-by-value mechanism makes a copy of the original array address and passes that, so you are just modifying the copy here(the original array address is quite unaffected. As a result, whenever you pass a one-dimensional array to a function, you are free to treat the value passed as a pointer in every sense, and change the address in any way that you want.
Passing Multi-Dimensional Arrays to a Function Passing a multi-dimensional array to a function is quite straightforward. The following statement declares a two dimensional array, beans: double beans[2][4];
You could then write the prototype of a hypothetical function, yield(), like this: double yield(double beans[2][4]);
245
Chapter 5 You may be wondering how the compiler can know that this is defining an array of the dimensions shown as an argument, and not a single array element. The answer is simple — you can’t write a single array element as a parameter in a function definition or prototype, although you can pass one as an argument when you call a function. For a parameter accepting a single element of an array as an argument, the parameter would have just a variable name. The array context doesn’t apply. When you are defining a multi-dimensional array as a parameter, you can also omit the first dimension value. Of course, the function needs some way of knowing the extent of the first dimension. For example, you could write this: double yield(double beans[][4], int index);
Here, the second parameter would provide the necessary information about the first dimension. The function can operate with a two-dimensional array with the value for the first dimension specified by the second argument to the function and with the second dimension fixed at 4.
Try It Out
Passing Multi-Dimensional Arrays
You define such a function in the following example: // Ex5_06.cpp // Passing a two-dimensional array to a function #include using std::cout; using std::endl; double yield(double array[][4], int n); int main(void) { double beans[3][4] =
{
{ 1.0, 2.0, 3.0, 4.0 }, { 5.0, 6.0, 7.0, 8.0 }, { 9.0, 10.0, 11.0, 12.0 }
};
cout << endl << “Yield = “ << yield(beans, sizeof beans/sizeof beans[0]); cout << endl; return 0; } // Function to compute total yield double yield(double beans[][4], int count) { double sum = 0.0; for(int i = 0; i < count; i++) // Loop through number of rows for(int j = 0; j < 4; j++) // Loop through elements in a row sum += beans[i][j]; return sum; }
The output from this example is: Yield = 78
246
Introducing Structure into Your Programs How It Works I have used different names for the parameters in the function header from those in the prototype, just to remind you that this is possible(but in this case, it doesn’t really improve the program at all. The first parameter is defined as an array of an arbitrary number of rows, each row having four elements. You actually call the function using the array beans with three rows. The second argument is specified by dividing the total size of the array in bytes by the size of the first row. This evaluates to the number of rows in the array. The computation in the function is simply a nested for loop with the inner loop summing elements of a single row and the outer loop repeating this for each row. Using a pointer in a function rather than a multi-dimensional array as an argument doesn’t really apply particularly well in this example. When the array is passed, it passes an address value which points to an array of four elements (a row). This doesn’t lend itself to an easy pointer operation within the function. You would need to modify the statement in the nested for loop to the following: sum += *(*(beans + i) + j);
so the computation is probably clearer in array notation.
References as Arguments to a Function We now come to the second of the two mechanisms for passing arguments to a function. Specifying a parameter to a function as a reference changes the method of passing data for that parameter. The method used is not pass-by-value, where an argument is copied before being transferred to the function, but pass-by-reference where the parameter acts as an alias for the argument passed. This eliminates any copying and allows the function to access the caller argument directly. It also means that the de-referencing, which is required when passing and using a pointer to a value, is also unnecessary.
Try It Out
Pass-by-reference
Let’s go back to a revised version of a very simple example, Ex5_03.cpp, to see how it would work using reference parameters: // Ex5_07.cpp // Using a reference to modify caller arguments #include using std::cout; using std::endl; int incr10(int& num);
// Function prototype
int main(void) { int num = 3; int value = 6; cout << endl
247
Chapter 5 << “incr10(num) = “ << incr10(num); cout << endl << “num = “ << num; cout << endl << “incr10(value) = “ << incr10(value); cout << endl << “value = “ << value; cout << endl; return 0; } // Function to increment a variable by 10 int incr10(int& num) // Function with reference argument { cout << endl << “Value received = “ << num; num += 10; return num;
// Increment the caller argument // - confidently // Return the incremented value
}
This program produces the output: Value received = 3 incr10(num) = 13 num = 13 Value received = 6 incr10(value) = 16 value = 16
How It Works You should find the way this works quite remarkable. This is essentially the same as Ex5_03.cpp, except that the function uses a reference as a parameter. The prototype has been changed to reflect this. When the function is called, the argument is specified just as though it was a pass-by-value operation, so it’s used in the same way as the earlier version. The argument value isn’t passed to the function. The function parameter is initialized with the address of the argument, so whenever the parameter num is used in the function, it accesses the caller argument directly. Just to reassure you that there’s nothing fishy about the use of the identifier num in main() as well as in the function, the function is called a second time with the variable value as the argument. At first sight, this may give you the impression that it contradicts what I said was a basic property of a reference — that after declared and initialized, it couldn’t be reassigned to another variable. The reason it isn’t contradictory is that a reference as a function parameter is created and initialized each time the function is called and is destroyed when the function ends, so you get a completely new reference created each time you use the function.
248
Introducing Structure into Your Programs Within the function, the value received from the calling program is displayed onscreen. Although the statement is essentially the same as the one used to output the address stored in a pointer, because num is now a reference, you obtain the data value rather than the address. This clearly demonstrates the difference between a reference and a pointer. A reference is an alias for another variable, and therefore can be used as an alternative way of referring to it. It is equivalent to using the original variable name. The output shows that the function incr10() is directly modifying the variable passed as a caller argument. You will find that if you try to use a numeric value, such as 20, as an argument to incr10(), the compiler outputs an error message. This is because the compiler recognizes that a reference parameter can be modified within a function, and the last thing you want is to have your constants changing value now and again. This would introduce a kind of excitement into your programs that you could probably do without. This security is all very well, but if the function didn’t modify the value, you wouldn’t want the compiler to create all these error messages every time you pass a reference argument that was a constant. Surely there ought to be some way to accommodate this? As Ollie would have said, ‘There most certainly is, Stanley!’
Use of the const Modifier You can apply the const modifier to a parameter to a function to tell the compiler that you don’t intend to modify it in any way. This causes the compiler to check that your code indeed does not modify the argument, and there are no error messages when you use a constant argument.
Try It Out
Passing a const
You can modify the previous program to show how the const modifier changes the situation. // Ex5_08.cpp // Using a reference to modify caller arguments #include using std::cout; using std::endl; int incr10(const int& num); int main(void) { const int num = 3; int value = 6;
// Function prototype
// Declared const to test for temporary creation
cout << endl << “incr10(num) = “ << incr10(num); cout << endl
249
Chapter 5 << “num = “ << num; cout << endl << “incr10(value) = “ << incr10(value); cout << endl << “value = “ << value; cout << endl; return 0; } // Function to increment a variable by 10 int incr10(const int& num) // Function with const reference argument { cout << endl << “Value received = “ << num; //
num += 10; return num+10;
// this statement would now be illegal // Return the incremented value
}
The output when you execute this is: Value received = 3 incr10(num) = 13 num = 3 Value received = 6 incr10(value) = 16 value = 6
How It Works You declare the variable num in main() as const to show that when the parameter to the function incr10() is declared as const, you no longer get a compiler message when passing a const object. It has also been necessary to comment out the statement that increments num in the function incr10(). If you uncomment this line, you’ll find the program no longer compiles because the compiler won’t allow num to appear on the left side of an assignment. When you specified num as const in the function header and prototype, you promised not to modify it, so the compiler checks that you kept your word. Everything works as before, except that the variables in main() are no longer changed in the function. By using reference arguments, you now have the best of both worlds. On one hand, you can write a function that can access caller arguments directly, and avoid the copying that is implicit in the pass-byvalue mechanism. On the other hand, where you don’t intend to modify an argument, you can get all the protection against accidental modification you need by using a const modifier with a reference.
Arguments to main() You can define main() with no parameters (or better, with the parameter list as void) or you can specify a parameter list that allows the main() function to obtain values from the command line from the execute
250
Introducing Structure into Your Programs command for the program. Values passed from the command line as arguments to main() are always interpreted as strings. If you want to get data into main() from the command line, you must define it like this: int main(int argc, char* argv[]) { // Code for main()... }
The first parameter is the count of the number strings found on the command line including the program name, and the second parameter is an array that contains pointers to these strings plus an additional element that is null. Thus argc is always at least 1 because you at least must enter the name of the program. The number of arguments received depends on what you enter on the command line to execute the program. For example, suppose that you execute the DoThat program with the command: DoThat.exe
There is just the name of the .exe file for the program so argc is 1 and the argv array contains two elements — argv[0] pointing to the string “DoThat.exe” and argv[1] that contains null. Suppose you enter this on the command line: DoThat or else “my friend” 999.9
Now argc is 5 and argv contains six elements, the last element being 0 and the first 5 pointing to the strings: “DoThat”
“or”
“else” “my friend”
“999.9”
You can see from this that if you want to have a string that includes spaces received as a single string you must enclose it between double quotes. You can also see that numerical values are read as strings so if you want conversion to the numerical value that is up to you. Let’s see it working.
Try It Out
Receiving Command-Line Arguments
This program just lists the arguments it receives from the command line. // Ex5_09.cpp // Reading command line arguments #include using std::cout; using std::endl; int main(int argc, char* argv[]) { cout << endl << “argc = “ << argc << endl; cout << “Command line arguments received are:” << endl; for(int i = 0 ; i <argc ; i++) cout << “argument “ << (i+1) << “: “ << argv[i] << endl; return 0; }
251
Chapter 5 You have two choices when entering the command-line arguments. After you build the example in the IDE, you can open a command window at the folder containing the .exe file and then enter the program name followed by the command-line arguments. Alternatively, you can specify the command-line arguments in the IDE before you execute the program. Just open the project properties window by selecting Project > Properties from the main menu and then extend the Configuration Properties tree in the left pane by clicking the plus sign (+). Click the Debugging folder to see where you can enter command line values in the right pane. Here is some typical output from this example entered in a command window: C:\Visual C++ 2005\Examples\Ex5_09 trying multiple “argument values” 4.5 0.0 argc = 6 Command line arguments received are: argument 1: Ex5_09 argument 2: trying argument 3: multiple argument 4: argument values argument 5: 4.5 argument 6: 0.0
How It Works The program first output the value of argc and then the values of each argument from the argv array in the for loop. You could make use of the fact that the last element in argv is null and code the output of the command line argument values like this: int i = 0; while(argv[i] != 0) cout << “argument “ << (i+1) << “: “ << argv[i++] << endl;
The while loop ends when argv[argc] is reached because that element is null.
Accepting a Variable Number of Function Arguments You can define a function so that it allows any number of arguments to be passed to it. You indicate that a variable number of arguments can be supplied when a function is called by placing an ellipsis (which is three periods, ...) at the end of the parameter lit in the function definition. For example: int sumValues(int first,...) { //Code for the function }
There must be at least one ordinary parameter, but you can have more. The ellipsis must always be placed at the end of the parameter list. Obviously there is no information about the type or number of arguments in the variable list, so your code must figure out what is passed to the function when it is called. The native C++ library defines va_start, va_arg, and va_end macros in the stdarg.h header to help you do this. It’s easiest to show how these are used with an example.
252
Introducing Structure into Your Programs Try It Out
Receiving a Variable Number of Arguments
This program uses a function that just sums the values of the arguments passed to it. // Ex5_10.cpp // Handling a variable number of arguments #include #include “stdarg.h” using std::cout; using std::endl; int sum(int count, ...) { if(count <= 0) return 0; va_list arg_ptr; va_start(arg_ptr, count);
// Declare argument list pointer // Set arg_ptr to 1st optional argument
int sum =0; for(int i = 0 ; i
// Add int value from arg_ptr and increment
va_end(arg_ptr); return sum;
// Reset the pointer to null
} int main(int argc, char* argv[]) { cout << sum(2, 4, 6, 8, 10, 12) << endl; cout << sum(11, 22, 33, 44, 55, 66, 77, 66, 99) << endl; }
This example produces the following output: 10 172630728 Press any key to continue . . .
How It Works The main() function calls the sum() function in the two output statements, in the first instance with six arguments and in the second with nine arguments. The sum() function has a single normal parameter of type int that represents the count of the number of arguments that follow. The ellipsis in the parameter list indicates an arbitrary number of arguments can be passed. Basically you have two ways of determining how many arguments there are when the function is called — you can require the number of arguments is specified by a fixed parameter as in the case of sum(), or you can require that the last argument has a special marker value that you can check for and recognize.
253
Chapter 5 To start processing the variable argument list you declare a pointer of type va_list: va_list arg_ptr;
// Declare argument list pointer
The va_list type is defined in the stdarg.h header file and the pointer is used to point to each argument in turn. The va_start macro is used to initialize arg_ptr so that it points to the first argument in the list: va_start(arg_ptr, count);
// Set arg_ptr to 1st optional argument
The second argument to the macro is the name of the fixed parameter that precedes the ellipsis in the parameter and this is used by the macro to determine where the first variable argument is. You retrieve the values of the arguments in the list in the for loop: int sum =0; for(int i = 0 ; i
// Add int value from arg_ptr and increment
The va_arg macro returns the value of the argument at the location specified by arg_ptr and increments arg_ptr to point to the next argument value. The second argument to the va_arg macro is the argument type, and this determines the value that you get as well as how arg_ptr increments so if this is not correct you get chaos; the program probably executes, but the values you retrieve are rubbish and arg_ptr is incremented incorrectly to access more rubbish. When you are finished retrieving argument values, you reset arg_ptr with the statement: va_end(arg_ptr);
// Reset the pointer to null
The va_end macro rests the pointer of type va_list that you pass as the argument to it to null. It’s a good idea to always do this because after processing the arguments arg_ptr points to a location that does not contain valid data.
Returning Values from a Function All the example functions that you have created have returned a single value. Is it possible to return anything other than a single value? Well, not directly, but as I said earlier, the single value returned needn’t be a numeric value; it could also be an address, which provides the key to returning any amount of data. You simply use a pointer. Unfortunately, this also is where the pitfalls start, so you need to keep your wits about you for the adventure ahead.
Returning a Pointer Returning a pointer value is easy. A pointer value is just an address, so if you want to return the address of some variable value, you can just write the following: return &value;
254
// Returning an address
Introducing Structure into Your Programs As long as the function header and function prototype indicate the return type appropriately, you have no problem — or at least no apparent problem. Assuming that the variable value is of type double, the prototype of a function called treble, which might contain the above return statement, could be as follows: double* treble(double data);
I have defined the parameter list arbitrarily here. So let’s look at a function that returns a pointer. It’s only fair that I warn you in advance — this function doesn’t work, but it is educational. Let’s assume that you need a function that returns a pointer to a memory location containing three times its argument value. Our first attempt the implement such a function might look like this: // Function to treble a value - mark 1 double* treble(double data) { double result = 0.0; result = 3.0*data; return &result; }
Try It Out
Returning a Bad Pointer
You could create a little test program to see what happens (remember that the treble function won’t work as expected): //Ex5_11.cpp #include using std::cout; using std::endl; double* treble(double);
// Function prototype
int main(void) { double num = 5.0; double* ptr = 0;
// Test value // Pointer to returned value
ptr = treble(num); cout << endl << “Three times num = “ << 3.0*num; cout << endl << “Result = “ << *ptr;
// Display 3*num
cout << endl; return 0;
255
Chapter 5 } // Function to treble a value - mark 1 double* treble(double data) { double result = 0.0; result = 3.0*data; return &result; }
There’s a hint that everything is not as it should be because compiling this program results in a warning from the compiler: warning C4172: returning address of local variable or temporary
The output that I got from executing the program was: Three times num = 15 Result = 4.10416e-230
How It Works (or Why It Doesn’t) The function main() calls the function treble() and stores the address returned in the pointer ptr, which should point to a value which is three times the argument, num. We then display the result of computing three times num, followed by the value at the address returned from the function. Clearly, the second line of output doesn’t reflect the correct value of 15, but where’s the error? Well, it’s not exactly a secret because the compiler gives fair warning of the problem. The error arises because the variable result in the function treble() is created when the function begins execution, and is destroyed on exiting from the function(so the memory that the pointer is pointing to no longer contains the original variable value. The memory previously allocated to result becomes available for other purposes, and here it has evidently been used for something else.
A Cast Iron Rule for Returning Addresses There is an absolutely cast iron rule for returning addresses: Never ever return the address of a local automatic variable from a function. You obviously can’t use a function that doesn’t work, so what can you do to rectify that? You could use a reference parameter and modify the original variable, but that’s not what you set out to do. You are trying to return a pointer to some useful data so that, ultimately, you can return more than a single item of data. One answer lies in dynamic memory allocation (you saw this in action in the last chapter). With the operator new, you can create a new variable in the free store that continued to exist until it is eventually destroyed by delete(or until the program ends. With this approach, the function looks like this: // Function to treble a value - mark 2 double* treble(double data) { double* result = new double(0.0);
256
Introducing Structure into Your Programs *result = 3.0*data; return result; }
Rather than declaring result as of type double, you now declare it to be of type double* and store in it the address returned by the operator new. Because the result is a pointer, the rest of the function is changed to reflect this, and the address contained in the result is finally returned to the calling program. You could exercise this version by replacing the function in the last working example with this version. You need to remember that with dynamic memory allocation from within a native C++ function such as this, more memory is allocated each time the function is called. The onus is on the calling program to delete the memory when it’s no longer required. It’s easy to forget to do this in practice, with the result that the free store is gradually eaten up until, at some point, it is exhausted and the program fails. As mentioned before, this sort of problem is referred to as a memory leak. Here you can see how the function would be used. The only necessary change to the original code is to use delete to free the memory as soon as you have finished with the pointer returned by the treble() function. #include using std::cout; using std::endl; double* treble(double);
// Function prototype
int main(void) { double num = 5.0; double* ptr = 0;
// Test value // Pointer to returned value
ptr = treble(num); cout << endl << “Three times num = “ << 3.0*num; cout << endl << “Result = “ << *ptr;
// Display 3*num
delete ptr;
// Don’t forget to free the memory
cout << endl; return 0; } // Function to treble a value - mark 2 double* treble(double data) { double* result = new double(0.0); *result = 3.0*data; return result; }
257
Chapter 5
Returning a Reference You can also return a reference from a function. This is just as fraught with potential errors as returning a pointer, so you need to take care with this too. Because a reference has no existence in its own right (it’s always an alias for something else), you must be sure that the object that it refers to still exists after the function completes execution. It’s very easy to forget this when you use references in a function because they appear to be just like ordinary variables. References as return types are of primary significance in the context of object-oriented programming. As you will see later in the book, they enable you to do things that would be impossible without them. (This particularly applies to “operator overloading,” which I’ll come to in Chapter 9.) The principal characteristic of a reference-type return value is that it’s an lvalue. This means that you can use the result of a function that returns a reference on the left side of an assignment statement.
Try It Out
Returning a Reference
Next, look at one example that illustrates the use of reference return types, and also demonstrates how a function can be used on the left of an assignment operation when it returns an lvalue. This example assumes that you have an array containing a mixed set of values. Whenever you want to insert a new value into the array, you want to replace the element with the lowest value. // Ex5_12.cpp // Returning a reference #include #include using std::cout; using std::endl; using std::setw; double& lowest(double values[], int length); // Prototype of function // returning a reference int main(void) { double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0, 4.5, 12.0, 6.8, 13.5, 2.1, 14.0 }; int len = sizeof array/sizeof array[0]; // Initialize to number // of elements cout << endl; for(int i = 0; i < len; i++) cout << setw(6) << array[i]; lowest(array, len) = 6.9; lowest(array, len) = 7.9; cout << endl; for(int i = 0; i < len; i++) cout << setw(6) << array[i]; cout << endl;
258
// Change lowest to 6.9 // Change lowest to 7.9
Introducing Structure into Your Programs return 0; } double& lowest(double a[], int len) { int j = 0; for(int i = 1; i < len; i++) if(a[j] > a[i]) j = i; return a[j];
// Index of lowest element // // // //
Test for a lower value... ...if so update j Return reference to lowest element
}
The output from this example is: 3 3
10 10
1.5 6.9
15 15
2.7 2.7
23 23
4.5 4.5
12 12
6.8 6.8
13.5 13.5
2.1 7.9
14 14
How It Works Let’s first take a look at how the function is implemented. The prototype for the function lowest() uses double& as the specification of the return type, which is therefore of type ‘reference to double’. You write a reference type return value in exactly the same way as you have already seen for variable declarations, appending the & to the data type. The function has two parameters specified — a one-dimensional array of type double and a parameter of type int that specifies the length of the array. The body of the function has a straightforward for loop to determine which element of the array passed contains the lowest value. The index, j, of the array element with the lowest value is arbitrarily set to 0 at the outset, and then modified within the loop if the current element, a[i], is less than a[j]. Thus, on exit from the loop, j contains the index value corresponding to the array element with the lowest value. The return statement is as follows: return a[j];
// Return reference to lowest element
In spite of the fact that this looks identical to the statement that would return a value, because the return type was declared as a reference, this returns a reference to the array element a[j] rather than the value that the element contains. The address of a[j] is used to initialize the reference to be returned. This reference is created by the compiler because the return type was declared as a reference. Don’t confuse returning &a[j] with returning a reference. If you write &a[j] as the return value, you are specifying the address of a[j], which is a pointer. If you do this after having specified the return type as a reference, you get an error message from the compiler. Specifically, you get this: error C2440: ‘return’ : cannot convert from ‘double *__w64 ‘ to ‘double &’
The function main(), which exercises the lowest()function, is very simple. An array of type double is declared and initialized with 12 arbitrary values, and an int variable len is initialized to the length of the array. The initial values in the array are output for comparison purposes. Again, the program uses the stream manipulator setw() to space the values uniformly, requiring the #include directive for .
259
Chapter 5 The function main() then calls the function lowest() on the left side of an assignment to change the lowest value in the array. This is done twice to show that it does actually work and is not an accident. The contents of the array are then output to the display again, with the same field width as before, so corresponding values line up. As you can see from the output with the first call to lowest(), the third element of the array, array[2], contained the lowest value, so the function returned a reference to it and its value was changed to 6.9. Similarly, on the second call, array[10] was changed to 7.9. This demonstrates quite clearly that returning a reference allows the use of the function on the left side of an assignment statement. The effect is as if the variable specified in the return statement appeared on the left of the assignment. Of course, if you want to, you can also use it on the right side of an assignment, or in any other suitable expression. If you had two arrays, X and Y, with the number of array elements specified by lenx and leny respectively, you could set the lowest element in the array x to twice the lowest element in the array y with this statement: lowest(x, lenx) = 2.0*lowest(y, leny);
This statement would call your lowest()function twice — once with arguments y and leny in the expression on the right side of the assignment and once with arguments x and lenx to obtain the address where the result of the right-hand expression is to be stored.
A Teflon-Coated Rule: Returning References A similar rule to the one concerning the return of a pointer from a function also applies to returning references: Never ever return a reference to a local variable from a function. I’ll leave the topic of returning a reference from a function for now, but I haven’t finished with it yet. I will come back to it again in the context of user-defined types and object-oriented programming, when you will unearth a few more magical things that you can do with references.
Static Variables in a Function There are some things you can’t do with automatic variables within a function. You can’t count how many times a function is called, for example, because you can’t accumulate a value from one call to the next. There’s more than one way to get around this if you need to. For instance, you could use a reference parameter to update a count in the calling program, but this wouldn’t help if the function was called from lots of different places within a program. You could use a global variable that you incremented from within the function, but globals are risky things to use, as they can be accessed from anywhere in a program, which makes it very easy to change them accidentally. Global variables are also risky in applications that have multiple threads of execution that access them, and you must take special care to manage how the globals are accessed from different threads. The basic problem that has to be addressed when more than one thread can access a global variable is that one thread can change the value of a global variable while another thread is working with it. The best solution in such circumstances is to avoid the use of global variables altogether.
260
Introducing Structure into Your Programs To create a variable whose value persists from one call of a function to the next, you can declare a variable within a function as static. You use exactly the same form of declaration for a static variable that you saw in Chapter 2. For example, to declare a variable count as static you could use this statement: static int count = 0;
This also initializes the variable to zero. Initialization of a static variable within a function only occurs the first time that the function is called. In fact, on the first call of a function, the static variable is created and initialized. It then continues to exist for the duration of program execution, and whatever value it contains when the function is exited is available when the function is next called.
Try It Out
Using Static Variables in Functions
You can demonstrate how a static variable behaves in a function with the following simple example: // Ex5_13.cpp // Using a static variable within a function #include using std::cout; using std::endl; void record(void);
// Function prototype, no arguments or return value
int main(void) { record(); for(int i = 0; i <= 3; i++) record(); cout << endl; return 0; } // A function that records how often it is called void record(void) { static int count = 0; cout << endl << “This is the “ << ++count; if((count > 3) && (count < 21)) // All this.... cout <<”th”; else switch(count%10) // is just to get... { case 1: cout << “st”; break; case 2: cout << “nd”; break; case 3: cout << “rd”;
261
Chapter 5 break; default: cout << “th”; } cout << “ time I have been called”; return;
// the right ending for... // 1st, 2nd, 3rd, 4th, etc.
}
Our function here serves only to record the fact that it was called. If you build and execute it, you get this output: This This This This This
is is is is is
the the the the the
1st 2nd 3rd 4th 5th
time time time time time
I I I I I
have have have have have
been been been been been
called called called called called
How It Works You initialize the static variable count with 0 and increment it in the first output statement in the function. Because the increment operation is prefixed, the incremented value is displayed by the output statement. It will be 1 on the first call, 2 on the second, and so on. Because the variable count is static, it continues to exist and retain its value from one call of the function to the next. The remainder of the function is concerned with working out when ‘st’, ‘nd’, ‘rd’, or ‘th’ should be appended to the value of count that is displayed. It’s surprisingly irregular. (I guess 101 should be 101st rather than 101th, shouldn’t it?) Note the return statement. Because the return type of the function is void, to include a value would cause a compiler error. You don’t actually need to put a return statement in this particular case as running off the closing brace for the body of the function is equivalent to the return statement without a value. The program would compile and run without error even if you didn’t include the return.
Recursive Function Calls When a function contains a call to itself it’s referred to as a recursive function. A recursive function call can also be indirect, where a function fun1 calls a function fun2, which in turn calls fun1. Recursion may seem to be a recipe for an infinite loop, and if you aren’t careful it certainly can be. An infinite loop will lock up your machine and require Ctrl-Alt-Del to end the program, which is always a nuisance. A prerequisite for avoiding an infinite loop is that the function contains some means of stopping the process. Unless you have come across the technique before, the sort of things to which recursion may be applied may not be obvious. In physics and mathematics there are many things that can be thought of as involving recursion. A simple example is the factorial of an integer which for a given integer N, is the product 1x2x3...xN. This is very often the example given to show recursion in operation. Recursion can also be applied to the analysis of programs during the compilation process; however, we will look at something even simpler.
262
Introducing Structure into Your Programs Try It Out
A Recursive Function
At the start of this chapter (see Ex5_01.cpp), you produced a function to compute the integral power of a value, that is, to compute xn. This is equivalent to x multiplied by itself n times. You can implement this as a recursive function as an elementary illustration of recursion in action. You can also improve the implementation of the function to deal with negative index values, where x-n is equivalent to 1/xn. // Ex5_14.cpp (based on Ex5_01.cpp) // A recursive version of x to the power n #include using std::cout; using std::endl; double power(double x, int n); int main(void) { double x = 2.0; double result = 0.0;
// Function prototype
// Different x from that in function power
// Calculate x raised to powers -3 to +3 inclusive for(int index = -3 ; index<=3 ; index++) cout << x << “ to the power “ << index << “ is “ << power(x, index)<< endl; return 0; } // Recursive function to compute integral powers of a double value // First argument is value, second argument is power index double power(double x, int n) { if(n < 0) { x = 1.0/x; n = -n; } if(n > 0) return x*power(x, n-1); else return 1.0; }
The output from this program is: 2 2 2 2 2 2 2
to to to to to to to
the the the the the the the
power power power power power power power
-3 is 0.125 -2 is 0.25 -1 is 0.5 0 is 1 1 is 2 2 is 4 3 is 8
263
Chapter 5 How It Works The function now supports positive and negative powers of x, so the first action is to check whether the value for the power that x is to be raised to, n, is negative: if(n < 0) { x = 1.0/x; n = -n; }
Supporting negative powers is easy; it just uses the fact that x-n can be evaluated as (1/x)n. Thus if n is negative, you set x to be 1.0/x and changed the sign of n so it’s positive. The next if statement decides whether or not the power() function should call itself once more: if(n > 0) return x*power(x, n-1); else return 1.0;
The if statement provides for the value 1.0 being returned if n is zero, and in all other cases it returns the result of the expression, x*power(x, n-1). This causes a further call to the function power() with the index value reduced by 1. Thus the else clause in the if statement provides the essential mechanism necessary to avoid an indefinite sequence of recursive function calls. Clearly, within the function power(), if the value of n is other than zero, a further call to the function power()occurs. In fact, for any given value of n other than 0, the function calls itself n times, ignoring the sign of n. The mechanism is illustrated in Figure 5-4, where the value 3 for the index argument is assumed. As you see, the power() function is called a total of four times to generate x3, three of the calls being recursive where the function is calling itself.
Using Recursion Unless you have a problem that particularly lends itself to using recursive functions, or if you have no obvious alternative, it’s generally better to use a different approach, such as a loop. This is much more efficient than using recursive function calls. Think about what happens with our last example to evaluate a simple product, x*x*...x, n times. On each call, the compiler generates copies of the two arguments to the function, and also has to keep track of the location to return to when each return is executed. It’s also necessary to arrange to save the contents of various registers in your computer so that they can be used within the function power(), and of course these need to be restored to their original state at each return from the function. With a quite modest depth of recursive call, the overhead can be considerably greater than if you use a loop. This is not to say you should never use recursion. Where the problem suggests the use of recursive function calls as a solution, it can be an immensely powerful technique, greatly simplifying the code. You’ll see an example where this is the case in the next chapter.
264
Introducing Structure into Your Programs Result: x3
x*x*x
power( x , 3 )
double power( double x, int n ) { ... return x*power( x , n - 1 ); ... }
x*x
2
double power( double x, int n ) { ... return x*power( x , n - 1 ); ... } x
1
double power( double x, int n ) { ... return x*power( x , n - 1 ); ... } 1.0
0
double power( double x, int n ) { ... return 1.0; ... }
Figure 5-4
C++/CLI Programming For the most part, functions in a C++/CLI program work in exactly the same way as in a native program. Of course, you deal in handles and tracking references when programming for the CLR not native pointers and references and that introduces some differences. There are a few other things that are a little different, so let’s itemize them. ❑
Function parameters and return values in a CLR program can be value class types, tracking handles, tracking references, and interior pointers.
❑
When a parameter is an array there no need to have a separate parameter for the size of the array because C++/CLI arrays have the size built into the Length property.
❑
You cannot do address arithmetic with array parameters in a C++/CLI program as you can in a native C++ program so you must always use array indexing.
265
Chapter 5 ❑
Returning a handle to memory you have allocated on the CLR heap is not a problem because the garbage collector takes care of releasing the memory when it is no longer in use.
❑
The mechanism for accepting a variable number of arguments in C++/CLI is different from the native C++ mechanism.
❑
Accessing command line arguments in main() in a C++/CLI program is also different from the native C++ mechanism.
Let’s look at the last two differences in more detail.
Functions Accepting a Variable Number of Arguments The C++/CLI language provides for a variable number of arguments by allowing you to specify the parameter list as an array with the array specification preceded by an ellipsis. Here’s an example of a function with this parameter list: int sum(... array^ args) { // Code for sum }
The sum() function here accepts any number of arguments of type int. To process the arguments you just access the elements of the array args. Because it is a CLR array, the number of elements is recorded as its Length property, so you have no problem determining the number of arguments in the body of the function. This mechanism is also an improvement over the native C++ mechanism you saw earlier because it is type-safe. The arguments clearly have to be of type int to be accepted by this function. Let’s try a variation on Ex5_10 to see the CLR mechanism working.
Try It Out
A Variable Number of Function Arguments
Here’s the code for this CLR project. // Ex5_15.cpp : main project file. // Passing a variable number of arguments to a function #include “stdafx.h” using namespace System; double sum(... array<double>^ args) { double sum = 0.0; for each(double arg in args) sum += arg; return sum; } int main(array<System::String ^> ^args) { Console::WriteLine(sum(2.0, 4.0, 6.0, 8.0, 10.0, 12.0)); Console::WriteLine(sum(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9)); return 0; }
266
Introducing Structure into Your Programs This example produces the following output: 42 49.5 Press any key to continue . . .
How It Works The sum() function here has been implemented to accept arguments of type double. The ellipsis preceding the array parameter tells the compiler to expect an arbitrary number of arguments and the argument values should be stored in an array of elements of type double. Of course, without the ellipsis, the function would expect just one argument when it was called that was a tracking handle for an array. Compared to the native version in Ex5_10 the definition of the sum() function is remarkably simple. All the problems associated with the type and the number of arguments have disappeared in the C++/CLI version. The sum is accumulated in a simple for each loop that iterates over all the elements in the array.
Arguments to main() You can see from the previous example that there is only one parameter to the main() function in a C++/CLI program and it is an array of elements of type String^. Accessing and processing commandline arguments in a C++/CLI program boils down to just accessing the elements in the array parameter. You can try it out.
Try It Out
Accessing Command Line Arguments
Here’s a C++/CLI version of Ex5_09. // Ex5_16.cpp : main project file. // Receiving multiple command liner arguments. #include “stdafx.h” using namespace System; int main(array<System::String ^> ^args) { Console::WriteLine(L”There were {0} command line arguments.”, args->Length); Console::WriteLine(L”Command line arguments received are:”); int i = 1; for each(String^ str in args) Console::WriteLine(L”Argument {0}: {1}”, i++, str); return 0; }
You can enter the command-line arguments in the command window or through the project properties window as described earlier in the chapter. This example produces the following output: C:\Visual C++ 2005\Examples\>Ex5_16 trying multiple “argument values” 4.5 0.0 There were 5 command line arguments. Command line arguments received are:
267
Chapter 5 Argument Argument Argument Argument Argument
1: 2: 3: 4: 5:
trying multiple argument values 4.5 0.0
How It Works From the output, you can see that one difference between this and the native C++ version is that you don’t get the program name passed to main() — not really a great disadvantage, really a positive feature in most circumstances. Accessing the command line arguments is now a trivial exercise involving just iterating through the elements in the args array.
Summar y In this chapter, you learned about the basics of program structure. You should have a good grasp of how functions are defined, how data can be passed to a function, and how results are returned to a calling program. Functions are fundamental to programming in C++, so everything you do from here on will involve using multiple functions in a program. The key points that you should keep in mind about writing your own functions are these: ❑
Functions should be compact units of code with a well-defined purpose. A typical program will consist of a large number of small functions, rather than a small number of large functions.
❑
Always provide a function prototype for each function defined in your program, positioned before you call that function.
❑
Passing values to a function using a reference can avoid the copying implicit in the call-by-value transfer of arguments. Parameters that are not modified in a function should be specified as const.
❑
When returning a reference or a pointer from a native C++ function, ensure that the object being returned has the correct scope. Never return a pointer or a reference to an object that is local to a native C++ function.
❑
In a C++/CLI program there is no problem with returning a handle to memory that has been allocated dynamically because the garbage collector takes care of deleting it when it is no longer required.
❑
When you pass a C++/CLI array to a function, there is no need for another parameter for the length of the array, as the number of elements is available in the function body as the Length property for the array.
The use of references as arguments is a very important concept, so make sure you are confident about using them. You’ll see a lot more about references as arguments to functions when we look into objectoriented programming.
268
Introducing Structure into Your Programs
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
The factorial of 4 (written as 4!) is 4*3*2*1 = 24, and 3! is 3*2*1 = 6, so it follows that 4! = 4*3!, or more generally: fact(n) = n*fact(n – 1) The limiting case is when n is 1, in which case 1! = 1. Write a recursive function which calculates factorials, and test it.
2.
Write a function that swaps two integers, using pointers as arguments. Write a program that uses this function and test that it works correctly.
3.
The trigonometry functions (sin(), cos() and tan()) in the standard math library take arguments in radians. Write three equivalent functions, called sind(), cosd() and tand(), which takes arguments in degrees. All arguments and return values should be type double.
4.
Write a native C++ program that reads a number (an integer) and a name (less than 15 characters) from the keyboard. Design the program so that the data entry is done in one function, and the output in another. Keep the data in the main program. The program should end when zero is entered for the number. Think about how you are going to pass the data between functions_by value, by pointer, or by reference?
5.
(Advanced) Write a function that, when passed a string consisting of words separated by single spaces, returns the first word; calling it again with an argument of NULL returns the second word, and so on, until the string has been processed completely, when NULL is returned. This is a simplified version of the way the native C++ run-time library routine strtok() works. So, when passed the string ‘one two three’, the function returns you ‘one’, then ‘two’, and finally ‘three’. Passing it a new string results in the current string being discarded before the function starts on the new string.
269
6 More about Program Structure In the previous chapter, you learned about the basics of defining functions and the various ways in which data can be passed to a function. You also saw how results are returned to a calling program. In this chapter, we will explore the further aspects of how functions can be put to good use, including: ❑
What a pointer to a function is
❑
How to define and use pointers to functions
❑
How to define and use arrays of pointers to functions
❑
What an exception is and how to write exception handlers that deal with them.
❑
How to write multiple functions with a single name to handle different kinds of data automatically
❑
What function templates are and how you define and use them
❑
How to write a substantial native C++ program example using several functions
❑
What generic functions are in C++/CLI
❑
How to write a substantial C++/CLI program example using several functions
Pointers to Functions A pointer stores an address value that, up to now, has been the address of another variable with the same basic type as the pointer. This has provided considerable flexibility in allowing you to use different variables at different times through a single pointer. A pointer can also point to the address of a function. This enables you to call a function through a pointer, which will be the function at the address that was last assigned to the pointer.
Chapter 6 Obviously, a pointer to a function must contain the memory address of the function that you want to call. To work properly, however, the pointer must also maintain information about the parameter list for the function it points to, as well as the return type. Therefore, when you declare a pointer to a function, you have to specify the parameter types and the return type of the functions that it can point to, in addition to the name of the pointer. Clearly, this is going to restrict what you can store in a particular pointer to a function. If you have declared a pointer to functions that accept one argument of type int and return a value of type double, you can only store the address of a function that has exactly the same form. If you want to store the address of a function that accepts two arguments of type int and returns type char, you must define another pointer with these characteristics.
Declaring Pointers to Functions You can declare a pointer pfun that you can use to point to functions that take two arguments, of type char* and int, and return a value of type double. The declaration would be as follows: double (*pfun)(char*, int);
// Pointer to function declaration
At first you may find that the parentheses make this look a little weird. This statement declares a pointer with the name pfun that can point to functions that accept two arguments of type pointer to char and of type int, and return a value of type double. The parentheses around the pointer name, pfun, and the asterisk are necessary; without them, the statement would be a function declaration rather than a pointer declaration. In this case, it would look like this: double *pfun(char*, int);
// Prototype for a function // returning type double*
This statement is a prototype for a function pfun() that has two parameters, and returns a pointer to a double value. Because you intended to declare a pointer, this is clearly not what you want at the moment. The general form of a declaration of a pointer to a function looks like this: return_type (*pointer_name)(list_of_parameter_types);
The pointer can only point to functions with the same return_type and list_of_parameter_types specified in the declaration. This shows that the declaration of a pointer to a function consists of three components: ❑
The return type of the functions that can be pointed to
❑
The pointer name preceded by an asterisk to indicate it is a pointer
❑
The parameter types of the functions that can be pointed to
If you attempt to assign a function to a pointer that does not conform to the types in the pointer declaration, the compiler generates an error message.
272
More about Program Structure You can initialize a pointer to a function with the name of a function within the declaration of the pointer. The following is an example of this: long sum(long num1, long num2); long (*pfun)(long, long) = sum;
// Function prototype // Pointer to function points to sum()
In general you can set the pfun pointer that you declared here to point to any function that accepts two arguments of type long and returns a value of type long. In the first instance you initialized it with the address of the sum() function that has the prototype given by the first statement. Of course, you can also initialize a pointer to a function by using an assignment statement. Assuming the pointer pfun has been declared as above, you could set the value of the pointer to a different function with these statements: long product(long, long); ... pfun = product;
// Function prototype // Set pointer to function product()
As with pointers to variables, you must ensure that a pointer to a function is initialized before you use it to call a function. Without initialization, catastrophic failure of your program is guaranteed.
Try It Out
Pointers to Functions
To get a proper feel for these newfangled pointers and how they perform in action, try one out in a program. // Ex6_01.cpp // Exercising pointers to functions #include using std::cout; using std::endl; long sum(long a, long b); long product(long a, long b);
// Function prototype // Function prototype
int main(void) { long (*pdo_it)(long, long);
// Pointer to function declaration
pdo_it = product; cout << endl << “3*5 = “ << pdo_it(3, 5);
// Call product thru a pointer
pdo_it = sum; // Reassign pointer to sum() cout << endl << “3*(4 + 5) + 6 = “ << pdo_it(product(3, pdo_it(4, 5)), 6); // Call thru a pointer, // twice cout << endl; return 0;
273
Chapter 6 } // Function to multiply two values long product(long a, long b) { return a*b; } // Function to add two values long sum(long a, long b) { return a + b; }
This example produces the output: 3*5 = 15 3*(4 + 5) + 6 = 33
How It Works This is hardly a useful program, but it does show very simply how a pointer to a function is declared, assigned a value, and subsequently used to call a function. After the usual preamble, you declare a pointer to a function, pdo_it, which can point to either of the other two functions that you have defined, sum() or product(). The pointer is given the address of the function product() in this assignment statement: pdo_it = product;
You just supply the name of the function as the initial value for the pointer and no parentheses or other adornments are required. The function name is automatically converted to an address, which is stored in the pointer. The function product() is called indirectly through the pointer pdo_it in the output statement. cout << endl << “3*5 = “ << pdo_it(3, 5);
// Call product thru a pointer
You use the name of the pointer just as if it was a function name, followed by the arguments between parentheses exactly as they would appear if you were using the original function name directly. Just to show that you can do it, you change the pointer to point to the function sum(). pdo_it = sum;
// Reassign pointer to sum()
You then use it again in a ludicrously convoluted expression to do some simple arithmetic: cout << endl << “3*(4 + 5) + 6 = “ << pdo_it(product(3, pdo_it(4, 5)), 6);
// Call thru a pointer, // twice
This shows that a pointer to a function can be used in exactly the same way as the function that it points to. The sequence of actions in the expression is shown in Figure 6-1.
274
More about Program Structure pdo_it ( product ( 3 , pdo_it ( 4 , 5 ) ) , 6) equivalent to
sum ( 4 , 5 )
results in
product ( 3 , 9 ) results in
pdo_it ( 27 , 6 )
equivalent to
sum ( 27 , 6 )
produces
33
Figure 6-1
A Pointer to a Function as an Argument Because ‘pointer to a function’ is a perfectly reasonable type, a function can also have a parameter that is a pointer to a function. The function can then call the function pointed to by the argument. Because the pointer can be made to point at different functions in different circumstances, this allows the particular function that is to be called from inside a function to be determined in the calling program. In this case, you can pass a function explicitly as an argument.
Try It Out
Passing a Function Pointer
You can look at this with an example. Suppose you need a function that processes an array of numbers by producing the sum of the squares of each of the numbers on some occasions, and the sum of the cubes on other occasions. One way of achieving this is by using a pointer to a function as an argument. //Ex6_02.cpp // A pointer to a function as an argument #include using std::cout; using std::endl; // Function prototypes double squared(double); double cubed(double);
275
Chapter 6 double sumarray(double array[], int len, double (*pfun)(double)); int main(void) { double array[] = { 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5 }; int len = sizeof array/sizeof array[0]; cout << endl << “Sum of squares = “ << sumarray(array, len, squared); cout << endl << “Sum of cubes = “ << sumarray(array, len, cubed); cout << endl; return 0; } // Function for a square of a value double squared(double x) { return x*x; } // Function for a cube of a value double cubed(double x) { return x*x*x; } // Function to sum functions of array elements double sumarray(double array[], int len, double (*pfun)(double)) { double total = 0.0; // Accumulate total in here for(int i = 0; i < len; i++) total += pfun(array[i]); return total; }
If you compile and run this code, you should see the following output: Sum of squares = 169.75 Sum of cubes = 1015.88
How It Works The first statement of interest is the prototype for the function sumarray(). Its third parameter is a pointer to a function that has a parameter of type double, and returns a value of type double. double sumarray(double array[], int len, double (*pfun)(double));
276
More about Program Structure The function sumarray() processes each element of the array passed as its first argument with whatever function is pointed to by its third argument. The function then returns the sum of the processed array elements. You call the function sumarray() twice in main(), the first time with the function name squared as the third argument, and the second time using cubed. In each case, the address corresponding to the function name that you use as the argument is substituted for the function pointer in the body of the function sumarray(), so the appropriate function is called within the for loop. There are obviously easier ways of achieving what this example does, but using a pointer to a function provides you with a lot of generality. You could pass any function to sumarray() that you care to define as long as it takes one double argument and returns a value of type double.
Arrays of Pointers to Functions In the same way as with regular pointers, you can declare an array of pointers to functions. You can also initialize them in the declaration. Here is an example of declaring an array of pointers. double double double double
sum(double, double); product(double, double); difference(double, double); (*pfun[3])(double,double) = { sum, product, difference };
// Function prototype // Function prototype // Function prototype // Array of function pointers
Each of the elements in the array is initialized by the corresponding function address appearing in the initializing list between braces. To call the function product() using the second element of the pointer array, you would write: pfun[1](2.5, 3.5);
The square brackets that select the function pointer array element appear immediately after the array name and before the arguments to the function being called. Of course, you can place a function call through an element of a function pointer array in any appropriate expression that the original function might legitimately appear in, and the index value selecting the pointer can be any expression producing a valid index value.
Initializing Function Parameters With all the functions you have used up to now, you have had to take care to provide an argument corresponding to each parameter in a function call. It can be quite handy to be able to omit one or more arguments in a function call and have some default values for the arguments that you leave out supplied automatically. You can arrange this by initializing the parameters to a function in its prototype. For example, suppose that you write a function to display a message, where the displayed message is passed as an argument. Here is the definition of such a function:
277
Chapter 6 void showit(const char message[]) { cout << endl << message; return; }
You can initialize the parameter to this function by specifying the initializing string value in the function prototype, as follows: void showit(const char message[] = “Something is wrong.”);
Here, the parameter message is initialized with the string literal shown. If you initialize a parameter to a function in the prototype, if you leave out that argument when you call the function, the initializing value is used in the call.
Try It Out
Omitting Function Arguments
Leaving out the function argument when you call the function executes it with the default value. If you supply the argument, it replaces the default value. You can use the showit() function to output a variety of messages. //Ex6_03.cpp // Omitting function arguments #include using std::cout; using std::endl; void showit(const char message[] = “Something is wrong.”); int main(void) { const char mymess[] = “The end of the world is nigh.”; showit(); showit(“Something is terribly wrong!”); showit(); showit(mymess); cout << endl; return 0; } void showit(const char message[]) { cout << endl << message; return; }
278
// // // //
Display Display Display Display
the basic message an alternative the default again a predefined message
More about Program Structure If you execute this example, it produces the following apocalyptic output: Something is wrong. Something is terribly wrong! Something is wrong. The end of the world is nigh.
How It Works As you can see, you get the default message specified in the function prototype whenever the argument is left out; otherwise, the function behaves normally. If you have a function with several arguments, you can provide initial values for as many of them as you like. If you want to omit more than one argument to take advantage of a default value, all arguments to the right of the leftmost argument that you omit must also be left out. For example, suppose you have this function: int do_it(long arg1 = 10, long arg2 = 20, long arg3 = 30, long arg4 = 40);
and you want to omit one argument in a call to it. you can omit only the last one, arg4. If you want to omit arg3, you must also omit arg4. If you omit arg2, arg3 and arg4 must also be omitted, and if you want to use the default value for arg1, you have to omit all of the arguments in the function call. You can conclude from this that you need to put the arguments which have default values in the function prototype together in sequence at the end of the parameter list, with the argument most likely to be omitted appearing last.
Exceptions If you’ve had a go at the exercises that appear at the end of the previous chapters, you’ve more than likely come across compiler errors and warnings, as well as errors that occur while the program is running. Exceptions are a way of flagging errors or unexpected conditions that occur in your C++ programs, and you already know that the new operator throws an exception if the memory you request cannot be allocated. So far, you have typically handled error conditions in your programs by using an if statement to test some expression, and then executing some specific code to deal with the error. C++ also provides another, more general mechanism for handling errors that allows you to separate the code that deals with these conditions from the code that executes when such conditions do not arise. It is important to realize that exceptions are not intended to be used as an alternative to the normal data checking and validating that you might do in a program. The code that is generated when you use exceptions carries quite a bit of overhead with it, so exceptions are really intended to be applied in the context of exceptional, near catastrophic conditions that might arise, but are not normally expected to occur in the normal course of events. An error reading from a disk might be something that you use exceptions for. An invalid data item being entered is not a good candidate for using exceptions.
279
Chapter 6 The exception mechanism uses three new keywords: ❑
try — identifies a code block in which an exception can occur
❑
throw — causes an exception condition to be originated
❑
catch — identifies a block of code in which the exception is handled
In the following Try It Out, you can see how they work in practice.
Try It Out
Throwing and Catching Exceptions
You can easily see how exception handling operates by working through an example. Let’s use a very simple context for this. Suppose that you are required to write a program that calculates the time it takes in minutes to make a part on a machine. The number of parts made in each hour is recorded, but you must keep in mind that the machine breaks down regularly and may not make any parts. You could code this using exception handling as follows: // Ex6_04.cpp Using exception handling #include using std::cout; using std::endl; int main(void) { int counts[] = {34, 54, 0, 27, 0, 10, 0}; int time = 60; // One hour in minutes for(int i = 0 ; i < sizeof counts/sizeof counts[0] ; i++) try { cout << endl << “Hour “ << i+1; if(counts[i] == 0) throw “Zero count - calculation not possible.”; cout << “ minutes per item: “ << static_cast<double>(time)/counts[i]; } catch(const char aMessage[]) { cout << endl << aMessage << endl; } return 0; }
If you run this example, the output is: Hour 1 minutes per item: 1.76471 Hour 2 minutes per item: 1.11111
280
More about Program Structure Hour 3 Zero count - calculation not possible. Hour 4 minutes per item: 2.22222 Hour 5 Zero count - calculation not possible. Hour 6 minutes per item: 6 Hour 7 Zero count - calculation not possible.
How It Works The code in the try block is executed in the normal sequence. The try block serves to define where an exception can be raised. You can see from the output that when an exception is thrown, the sequence of execution continues with the catch block and after the code in the catch block has been executed, execution continues with the next loop iteration. Of course, when no exception is thrown, the catch block is not executed. Both the try block and the catch block are regarded as a single unit by the compiler, so they both form the for loop block and the loop continues after an exception is thrown. The division is carried out in the output statement that follows the if statement checking the divisor. When a throw statement is executed, control passes immediately to the first statement in the catch block, so the statement that performs the division is bypassed when an exception is thrown. After the statement in the catch block executes, the loop continues with the next iteration if there is one.
Throwing Exceptions Exceptions can be thrown anywhere within a try block, and the operand of the throw statements determines a type for the exception — the exception thrown in the example is a string literal and therefore of type const char[]. The operand following the throw keyword can be any expression, and the type of the result of the expression determines the type of exception thrown. Exceptions can also be thrown in functions called from within a try block and caught by a catch block following the try block. You could add a function to the previous example to demonstrate this, with the definition: void testThrow(void) { throw “ Zero count - calculation not possible.”; }
You place a call to this function in the previous example in place of the throw statement: if(counts[i] == 0) testThrow();
// Call a function that throws an exception
The exception is thrown by the testThrow() function and caught by the catch block whenever the array element is zero, so the output is the same as before. Don’t forget the function prototype if you add the definition of testThrow() to the end of the source code.
281
Chapter 6
Catching Exceptions The catch block following the try block in our example catches any exception of type const char[]. This is determined by the parameter specification that appears in parentheses following the keyword catch. You must supply at least one catch block for a try block, and the catch blocks must immediately follow the try block. A catch block catches all exceptions (of the correct type) that occur anywhere in the code in the immediately preceding try block, including those thrown in any functions called directly or indirectly within the try block. If you want to specify that a catch block is to handle any exception thrown in a try block, you must put an ellipsis (...) between the parentheses enclosing the exception declaration: catch (...) { // code to handle any exception }
This catch block must appear last if you have other catch blocks defined for the try block.
Try It Out
Nested try Blocks
You can nest try blocks one within another. With this situation, if an exception is thrown from within an inner try block that is not followed by a catch block corresponding to the type of exception thrown, the catch handlers for the outer try block are searched. You can demonstrate this with the following example: // Ex6_05.cpp // Nested try blocks #include using std::cin; using std::cout; using std::endl; int main(void) { int height = 0; const double inchesToMeters = 0.0254; char ch = ‘y’; try { while(ch == ‘y’||ch ==’Y’) { cout << “Enter a height in inches: “; cin >> height; try { if(height > 100) throw “Height exceeds maximum”; if(height < 9) throw height;
// Outer try block
// Read the height to be converted // Defines try block in which // exceptions may be thrown // Exception thrown // Exception thrown
cout << static_cast<double>(height)*inchesToMeters
282
More about Program Structure << “ meters” << endl; } catch(const char aMessage[]) // start of catch block which { // catches exceptions of type cout << aMessage << endl; // const char[] } cout << “Do you want to continue(y or n)?”; cin >> ch; } } catch(int badHeight) { cout << badHeight << “ inches is below minimum” << endl; } return 0; }
Here there is a try block enclosing the while loop and an inner try block in which two different types of exception may be thrown. The exception of type const char[] is caught by the catch block for the inner try block, but the exception of type int has no catch handler associated with the inner try block; therefore, the catch handler in the outer try block is executed. In this case, the program ends immediately because the statement following the catch block is a return.
Exception Handling in the MFC This is a good point to raise the question of MFC and exceptions because they are used to some extent. If you browse the documentation that came with Visual C++ 2005, you may come across TRY, THROW, and CATCH in the index These are macros defined within MFC that were created before the exception handling was implemented in the C++ language. They mimic the operation of try throw and catch in the C++ language, but the language facilities for exception handling really render these obsolete so you should not use them. They are, however, still there for two reasons. There are large numbers of programs still around that use these macros, and it is important to ensure that as far as possible old code still compiles. Also, most of the MFC that throws exceptions was implemented in terms of these macros. In any event, any new programs should use the try, throw, and catch keywords in C++ because they work with the MFC. There is one slight anomaly you need to keep in mind when you use MFC functions that throw exceptions. The MFC functions that throw exceptions generally throw exceptions of class types(you will find out about class types before you get to use the MFC. Even though the exception that an MFC function throws is of a given class type(CDBException say(you need to catch the exception as a pointer, not as the type of the exception. So with the exception thrown being of type CDBException, the type that appears as the catch block parameter is CBDException*. You will see examples where this is the case in context later in the book.
283
Chapter 6
Handling Memor y Allocation Errors When you used the operator new to allocate memory for our variables (as you saw in Chapters 4 and 5), you ignored the possibility that the memory might not be allocated. If the memory isn’t allocated, an exception is thrown that results in the termination of the program. Ignoring this exception is quite acceptable in most situations because having no memory left is usually a terminal condition for a program that you can usually do nothing about. However, there can be circumstances where you might be able to do something about it if you had the chance or you might want to report the problem in your own way. In this situation, you can catch the exception that the new operator throws. Let’s contrive an example to show this happening.
Try It Out
Catching an Exception Thrown by the new Operator
The exception that the new operator throws when memory cannot be allocated is of type bad_alloc. bad_alloc is a class type defined in the standard header file, so you’ll need an #include directive for that. Here’s the code: // Ex6_06.cpp #include #include using std::bad_alloc; using std::cout; using std::endl;
// For bad_alloc type
int main( ) { char* pdata = 0; size_t count = ~static_cast<size_t>(0)/2; try { pdata = new char[count]; cout << “Memory allocated.” << endl; } catch(bad_alloc &ex) { cout << “Memory allocation failed.” << endl << “The information from the exception object is: “ << ex.what() << endl; } delete[] pdata; return 0; }
On my machine this example produces the following output: Memory allocation failed. The information from the exception object is: bad allocation
If you are in the fortunate position of having many gigabytes of memory in your computer, you may not get the exception thrown.
284
More about Program Structure How It Works The example allocates memory dynamically for an array of type char[] where the length is specified by the count variable that you define as: size_t count = ~static_cast<size_t>(0)/2;
The size of an array is an integer of type size_t so you declare count to be of this type. The value for count is generated by a somewhat complicated expression. The value 0 is type int so the value produced by the expression static_cast<size_t>(0) is a zero of type size_t. Applying the ~ operator to this flips all the bits so you then have a size_t value with all the bits as 1, which corresponds to the maximum value you can represent as size_t because size_t is an unsigned type. This value exceeds the maximum amount of memory that the new operator can allocate in one go so you divide by 2 to bring it within the bounds of what is possible. This is still a very large value so unless your machine is exceptionally well endowed with memory, the allocation request will fail. The allocation of the memory takes place in the try block. If the allocation succeeds you’ll see a message to that effect but if as you expect it fails, an exception of type bad_alloc will be thrown by the new operator. This causes the code in the catch block to be executed. Calling the what() function for the bad_alloc object reference ex returns a string describing the problem that caused the exception and you see the result of this call in the output. Most exception classes implement the what() function to provide a string describing why the exception was thrown. To handle out-of-memory situations with some positive effect, clearly you must have some means of returning memory to the free store. In most practical cases, this involves some serious work on the program to manage memory so it is not often undertaken.
Function Overloading Suppose you have written a function that determines the maximum value in an array of values of type double: // Function to generate the maximum value in an array of type double double maxdouble(double array[], int len) { double max = array[0]; for(int i = 1; i < len; i++) if(max < array[i]) max = array[i]; return max; }
You now want to create a function that produces the maximum value from an array of type long, so you write another function similar to the first, with this prototype: long maxlong(long array[], int len);
285
Chapter 6 You have chosen the function name to reflect the particular task in hand, which is OK for two functions, but you may also need the same function for several other types of argument. It seems a pity that you have to keep inventing names. Ideally, you would use the same function name max() regardless of the argument type, and have the appropriate version executed. It probably won’t be any surprise to you that you can indeed do this, and the C++ mechanism that makes it possible is called function overloading.
What Is Function Overloading? Function overloading allows you to use the same function name for defining several functions as long as they each have different parameter lists. When the function is called, the compiler chooses the correct version for the job base on the list of arguments you supply. Obviously, the compiler must always be able to decide unequivocally which function should be selected in any particular instance of a function call, so the parameter list for each function in a set of overloaded functions must be unique. Following on from the max() function example, you could create overloaded functions with the following prototypes: int max(int array[], int len); long max(long array[], int len); double max(double array[], int len);
// Prototypes for // a set of overloaded // functions
These functions share a common name, but have a different parameter list. In general, overloaded functions can be differentiated by having corresponding parameters of different types, or by having a different number of parameters. Note that a different return type does not distinguish a function adequately. You can’t add the following function to the previous set: double max(long array[], int len);
// Not valid overloading
The reason is that this function would be indistinguishable from the function that has this prototype: long max(long array[], int len);
If you define functions like this, it causes the compiler to complain with the following error: error C2556: ‘double max(long [],int)’ : overloaded function differs only by return type from ‘long max(long [],int)’
and the program does not compile. This may seem slightly unreasonable, until you remember that you can write statements such as these: long numbers[] = {1, 2, 3, 3, 6, 7, 11, 50, 40}; int len = sizeof numbers/sizeof numbers[0]; max(numbers, len);
The fact that the call for the max() function doesn’t make much sense here because you discard the result does not make it illegal. If the return type were permitted as a distinguishing feature, the compiler would be unable to decide whether to choose the version with a long return type or a double return type in the instance of the preceding code. For this reason the return type is not considered to be a differentiating feature of overloaded functions.
286
More about Program Structure In fact every function(not just overloaded functions(is said to have a signature, where the signature of a function is determined by its name and its parameter list. All functions in a program must have unique signatures; otherwise the program does not compile.
Try It Out
Using Overloaded Functions
You can exercise the overloading capability with the function max() that you have already defined. Try an example that includes the three versions for int, long and double arrays. // Ex6_07.cpp // Using overloaded functions #include using std::cout; using std::endl; int max(int array[], int len); long max(long array[], int len); double max(double array[], int len);
// Prototypes for // a set of overloaded // functions
int main(void) { int small[] = {1, 24, 34, 22}; long medium[] = {23, 245, 123, 1, 234, 2345}; double large[] = {23.0, 1.4, 2.456, 345.5, 12.0, 21.0}; int lensmall = sizeof small/sizeof small[0]; int lenmedium = sizeof medium/sizeof medium[0]; int lenlarge = sizeof large/sizeof large[0]; cout << endl << max(small, lensmall); cout << endl << max(medium, lenmedium); cout << endl << max(large, lenlarge); cout << endl; return 0; } // Maximum of ints int max(int x[], int len) { int max = x[0]; for(int i = 1; i < len; i++) if(max < x[i]) max = x[i]; return max; } // Maximum of longs long max(long x[], int len) { long max = x[0]; for(int i = 1; i < len; i++) if(max < x[i])
287
Chapter 6 max = x[i]; return max; } // Maximum of doubles double max(double x[], int len) { double max = x[0]; for(int i = 1; i < len; i++) if(max < x[i]) max = x[i]; return max; }
The example works as you would expect and produces this output: 34 2345 345.5
How It Works You have three prototypes for the three overloaded versions of the function max(). In each of the three output statements, the appropriate version of the function max() is selected by the compiler based on the argument list types. This works because each of the versions of the max() function has a unique signature because its parameter list is different from that of the other max() functions.
When to Overload Functions Function overloading provides you with the means of ensuring that a function name describes the function being performed and is not confused by extraneous information such as the type of data being processed. This is akin to what happens with basic operations in C++. To add two numbers you use the same operator, regardless of the types of the operands. Our overloaded function max() has the same name, regardless of the type of data being processed. This helps to make the code more readable and makes these functions easier to use. The intent of function overloading is clear: to enable the same operation to be performed with different operands using a single function name. So, whenever you have a series of functions that do essentially the same thing, but with different types of arguments, you should overload them and use a common function name.
Function Templates The last example was somewhat tedious in that you had to repeat essentially the same code for each function, but with different variable and parameter types, however, there is a way of avoiding this. You have the possibility of creating a recipe that will enable the compiler to automatically generate functions with various parameter types. The code defining the recipe for generating a particular group of functions is called a function template.
288
More about Program Structure A function template has one or more type parameters, and you generate a particular function by supplying a concrete type argument for each of the template’s parameters. Thus the functions generated by a function template all have the same basic code, but customized by the type arguments that you supply. You can see how this works in practice by defining a function template for the function max() in the previous example.
Using a Function Template You can define a template for the function max() as follows: template T max(T x[], int len) { T max = x[0]; for(int i = 1; i < len; i++) if(max < x[i]) max = x[i]; return max; }
The template keyword identifies this as a template definition. The angled brackets following the template keyword enclose the type parameters that are used to create a particular instance of the function separated by commas; in this instance you have just one type parameter, T. The keyword class before the T indicates that the T is the type parameter for this template, class being the generic term for type. Later in the book you will see that defining a class is essentially defining your own data type. Consequently, you have fundamental types in C++, such as type int and type char, and you also have the types that you define yourself. Note that you can use the keyword typename instead of class to identify the parameters in a function template, in which case the template definition would look like this: template T max(T x[], int len) { T max = x[0]; for(int i = 1; i < len; i++) if(max < x[i]) max = x[i]; return max; }
Some programmers prefer to use the typename keyword as the class keyword tends to connote a userdefined type, whereas typename is more neutral and therefore is more readily understood to imply fundamental types as well as user-defined types. In practice you’ll see both keywords used widely. Wherever T appears in the definition of a function template, it is replaced by the specific type argument, such as long, that you supply when you create an instance of the template. If you try this out manually by plugging in long in place of T in the template, you’ll see that this generates a perfectly satisfactory function for calculating the maximum value from an array of type long: long max(long x[], int len) { long max = x[0]; for(int i = 1; i < len; i++) if(max < x[i])
289
Chapter 6 max = x[i]; return max; }
The creation of a particular function instance is referred to as instantiation. Each time you use the function max() in your program, the compiler checks to see if a function corresponding to the type of arguments that you have used in the function call already exists. If the function required does not exist, the compiler creates one by substituting the argument type that you have used in your function call in place of the parameter T throughout the source code in the corresponding template definition. You could exercise the template for max() function with the same main() function that you used in the previous example.
Try It Out
Using a Function Template
Here’s a version of the previous example modified to use a template for the max() function: // Ex6_08.cpp // Using function templates #include using std::cout; using std::endl; // Template for function to compute the maximum element of an array template T max(T x[], int len) { T max = x[0]; for(int i = 1; i < len; i++) if(max < x[i]) max = x[i]; return max; } int main(void) { int small[] = { 1, 24, 34, 22}; long medium[] = { 23, 245, 123, 1, 234, 2345}; double large[] = { 23.0, 1.4, 2.456, 345.5, 12.0, 21.0}; int lensmall = sizeof small/sizeof small[0]; int lenmedium = sizeof medium/sizeof medium[0]; int lenlarge = sizeof large/sizeof large[0]; cout << endl << max(small, lensmall); cout << endl << max(medium, lenmedium); cout << endl << max(large, lenlarge); cout << endl; return 0; }
If you run this program, it produces exactly the same output as the previous example.
290
More about Program Structure How It Works For each of the statements outputting the maximum value in an array, a new version of max() is instantiated using the template. Of course, if you add another statement calling the function max() with one of the types used previously, no new version of the code is generated. Note that using a template doesn’t reduce the size of your compiled program in any way. The compiler generates a version of the source code for each function that you require. In fact, using templates can generally increase the size of your program, as functions can be created automatically even though an existing version might satisfactorily be used by casting the argument accordingly. You can force the creation of particular instances of a template by explicitly including a declaration for it. For example, if you wanted to ensure that an instance of the template for the function max() was created corresponding to the type float, you could place the following declaration after the definition of the template: float max(float, int);
This forces the creation of this version of the function template. It does not have much value in the case of our program example, but it can be useful when you know that several versions of a template function might be generated, but you want to force the generation of a subset that you plan to use with arguments cast to the appropriate type where necessary.
An Example Using Functions You have covered a lot of ground in C++ up to now and a lot on functions in this chapter alone. After wading through a varied menu of language capabilities, it’s not always easy to see how they relate to one another. Now would be a good point to see how some of this goes together to produce something with more meat than a simple demonstration program. Let’s work through a more realistic example to see how a problem can be broken down into functions. The process involves defining the problem to be solved, analyzing the problem to see how it can be implemented in C++, and finally writing the code. The approach here is aimed at illustrating how various functions go together to make up the final result, rather than providing a tutorial on how to develop a program.
Implementing a Calculator Suppose you need a program that acts as a calculator; not one of these fancy devices with lots of buttons and gizmos designed for those who are easily pleased, but one for people who know where they are going, arithmetically speaking. You can really go for it and enter a calculation from the keyboard as a single arithmetic expression, and have the answer displayed immediately. An example of the sort of thing that you might enter is: 2*3.14159*12.6*12.6 / 2 + 25.2*25.2
To avoid unnecessary complications for the moment, you won’t allow parentheses in the expression and the whole computation must be entered in a single line; however, to allow the user to make the input look attractive, you will allow spaces to be placed anywhere. The expression entered may contain the
291
Chapter 6 operators multiply, divide, add, and subtract represented by *, /, + and — respectively, and the expression entered will be evaluated with normal arithmetic rules, so that multiplication and division take precedence over addition and subtraction. The program should allow as many successive calculations to be performed as required, and should terminate if an empty line is entered. It should also have helpful and friendly error messages.
Analyzing the Problem A good place to start is with the input. The program reads in an arithmetic expression of any length on a single line, which can be any construction within the terms given. Because nothing is fixed about the elements making up the expression, you have to read it as a string of characters and then work out within the program how it’s made up. You can decide arbitrarily that you will handle a string of up to 80 characters, so you could store it in an array declared within these statements: const int MAX = 80; char buffer[MAX];
// Maximum expression length including ‘\0’ // Input area for expression to be evaluated
To change the maximum length of the string processed by the program, you will only need to alter the initial value of MAX. You need to understand the basic structure of the information that appears in the input string, so let’s break it down step-by-step. Make sure that the input is as uncluttered as possible when you are processing it, so before you start analyzing the input string, you will get rid of any spaces in it. You can call the function that will do this eatspaces(). This function can work by stepping through the input buffer — which is the array buffer[] — and shuffling characters up to overwrite any spaces. This process uses two indexes to buffer array, i and j, which start out at the beginning of the buffer; in general, you’ll store element j at position i. As you progress through the array elements, each time you find a space you increment j but not i, so the space at position i gets overwritten by the next character you find at index position j that is not a space. Figure 6-2 illustrates the logic of this. The buffer array before copying its contents to itself index j Index i is not incremented at these positions because they contain a space. These spaces are overwritten by the next non-space character that is found in buffer.
index i
0 1 2 3 4 5 6 7 2 + 5 * 3 \0
2 + 5 * 3 \0 0 1 2 3 4 5
.....
.....
The buffer array after copying its contents to itself
Figure 6-2
292
More about Program Structure This process is one of copying the contents of the array buffer[] to itself, excluding any spaces. Figure 6-2 shows the buffer array before and after the copying process and the arrows indicate which characters are copied and the position to which each character is copied. When you have removed spaces from the input you are ready to evaluate the expression. You define the function expr() which returns the value that results from evaluating the whole expression in the input buffer. To decide what goes on inside the expr() function, you need to look into the structure of the input in more detail. The add and subtract operators have the lowest precedence and so are evaluated last. You can think of the input string as comprising one or more terms connected by operators, which can be either the operator + or the operator -. You can refer to either operator as an addop. With this terminology, you can represent the general form of the input expression like this: expression: term addop term ... addop term
The expression contains at least one term and can have an arbitrary number of following addop term combinations. In fact, assuming that you have removed all the blanks, there are only three legal possibilities for the character that follows each term: ❑
The next character is ‘\0’, so you are at the end of the string
❑
The next character is ‘-’, in which case you should subtract the next term from the value accrued for the expression up to this point
❑
The next character is ‘+’, in which case you should add the value of the next term to the value of the expression accumulated so far
If anything else follows a term, the string is not what you expect, so you’ll display an error message and exit from the program. Figure 6-3 illustrates the structure of a sample expression. 2 + 5 * 3 - 7 / 2 \0
.....
End of input term
term
addop
term addop
Figure 6-3
Next, you need a more detailed and precise definition of a term. A term is simply a series of numbers connected by either the operator * or the operator /. Therefore, a term (in general) looks like this: term: number multop number ... multop number
293
Chapter 6 multop represents either a multiply or a divide operator. You could define a function term() to return the value of a term. This needs to scan the string to first a number and then to look for a multop followed by another number. If a character is found that isn’t a multop, the term() function assumes that it is an addop and return the value that has been found up to that point.
The last thing you need to figure out before writing the program is how you recognize a number. To minimize the complexity of the code, you’ll only recognize unsigned numbers; therefore, a number consists of a series of digits that may be optionally followed by a decimal point and some more digits. To determine the value of a number you step through the buffer looking for digits. If you find anything that isn’t a digit, you check whether it’s a decimal point. If it’s not a decimal point it has nothing to do with a number, so you return what you have got. If you find a decimal point, you look for more digits. As soon as you find anything that’s not a digit, you have the complete number and you return that. Imaginatively, you’ll call the function to recognize a number and return its value number(). Figure 6-4 shows an example of how an expression breaks down into terms and numbers. expression term addop
term
number
number
digit digit
digit point digit
2
3
+
3
.
addop
multop number
5
number
digit digit
*
1
7
term
digit digit
-
1
The value of the expression is returned by the expr() function The value of each term is returned by the term() function The value of each number is returned by the number() function
.....
2 \0
End of input
Figure 6-4
You now have enough understanding of the problem to write some code. You can work through the functions you need and then write a main() function to tie them all together. The first and perhaps easiest function to write is eatspaces(), which is going to eliminate the blanks from the input string.
Eliminating Blanks from a String You can write the prototype for the eatspaces() function as follows: void eatspaces(char* str);
294
// Function to eliminate blanks
More about Program Structure The function doesn’t need to return any value because the blanks can be eliminated from the string in situ, modifying the original string directly through the pointer that is passed as the argument. The process for eliminating blanks is a very simple one. You copy the string to itself, overwriting any spaces as you saw earlier in this chapter. You can define the function to do this as follows: // Function to eliminate spaces from a string void eatspaces(char* str) { int i = 0; int j = 0; while((*(str + i) = *(str + j++)) != ‘\0’) if(*(str + i) != ‘ ‘) i++; return;
// ‘Copy to’ index to string // ‘Copy from’ index to string // Loop while character is not \0 // Increment i as long as // character is not a space
}
How the Function Functions All the action is in the while loop. The loop condition copies the string by moving the character at position j to the character at position i and then increments j to the next character. If the character copied was ‘\0’, you have reached the end of the string and you’re done. The only action in the loop statement is to increment i to the next character if the last character copied was not a blank. If it is a blank, i is not be incremented and the blank can therefore be overwritten by the character copied on the next iteration. That wasn’t hard, was it? Next, you can try writing the function that returns the result of evaluating the expression.
Evaluating an Expression The expr()function returns the value of the expression specified in the string that is supplied as an argument, so you can write its prototype as follows: double expr(char* str);
// Function evaluating an expression
The function declared here accepts a string as an argument and returns the result as type double. Based on the structure for an expression that you worked out earlier, you can draw a logic diagram for the process of evaluating an expression as shown in Figure 6-5.
295
Chapter 6 Get value of first term
Set expression value to value of first term
Next character is '/0'?
Return expression value
Next character is '-'?
Subtract value of next term from expression value
Next character is '+'?
Add value of next term from expression value
ERROR Figure 6-5
Using this basic definition of the logic, you can now write the function: // Function to evaluate an arithmetic expression double expr(char* str) { double value = 0.0; // Store result here int index = 0; // Keeps track of current character position
296
value = term(str, index);
// Get first term
for(;;)
// Indefinite loop, all exits inside
More about Program Structure { switch(*(str + index++)) { case ‘\0’: return value;
// Choose action based on current character // We’re at the end of the string // so return what we have got
case ‘+’: value += term(str, index); break;
// + found so add in the // next term
case ‘-’: value -= term(str, index); break;
// - found so subtract // the next term
default: // If we reach here the string cout << endl // is junk << “Arrrgh!*#!! There’s an error” << endl; exit(1); } } }
How the Function Functions Considering this function is analyzing any arithmetic expression that you care to throw at it (as long as it uses our operator subset), it’s not a lot of code. You define a variable index of type int, which keeps track of the current position in the string where you are working, and you initialize it to 0, which corresponds to the index position of the first character in the string. You also define a variable value of type double in which you’ll accumulate the value of the expression that is passed to the function in the char array str. Because an expression must have at least one term, the first action in the function is to get the value of the first term by calling the function term(), which you have yet to write. This actually places three requirements on the function term():
1.
It should accept a char* pointer and an int variable as parameters, the second parameter being an index to the first character of the term in the string supplied.
2.
It should update the index value passed to position it at the character following the last character of the term found.
3.
It should return the value of the term as type double.
The rest of the program is an indefinite for loop. Within the loop, the action is determined by a switch statement, which is controlled by the current character in the string. If it is a ‘+’, you call the term()function to get the value of the next term in the expression and add it to the variable value. If it is a ‘-’, we subtract the value returned by term() from the variable value. If it is a ‘\0’, you are at the end of the string, so you return the current contents of the variable value to the calling program. If it is any other character, it shouldn’t be there, so after remonstrating with the user you end the program! As long as either a ‘+’ or a ‘-’ is found, the loop continues. Each call to term() moves the value of the index variable to the character following the term that was evaluated, and this should be should be either another ‘+’ or ‘-’, or the end of string character ‘\0’. Thus, the function either terminates
297
Chapter 6 normally when ‘\0’ is reached, or abnormally by calling exit(). You need to remember the #include directive for header file that provides the definition of the function exit() when you come to put the whole program together. You also could also analyze an arithmetic expression using a recursive function. If you think about the definition of an expression slightly differently, you could specify it as being either a term, or a term followed by an expression. The definition here is recursive (i.e. the definition involves the item being defined), and this approach is very common in defining programming language structures. This definition provides just as much flexibility as the first, but using it as the base concept, you could arrive at a recursive version of expr() instead of using a loop as you did in the implementation above. You might want to try this alternative approach as an exercise after you have completed the first version.
Getting the Value of a Term The term() function returns a value for a term as type double and receive two arguments: the string being analyzed and an index to the current position in the string. There are other ways of doing this, but this arrangement is quite straightforward. You can, therefore, write the prototype of the function term() as follows: double term(char* str, int& index);
// Function analyzing a term
You have specified the second parameter as a reference. This is because you want the function to be able to modify the value of the variable index in the calling program to position it at the character following the last character of the term found in the input string. You could return index as a value, but then you would need to return the value of the term in some other way, so this arrangement seems quite natural. The logic for analyzing a term is going to be similar in structure to that for an expression. A term is a number, potentially followed by one or more combinations of a multiply or a divide operator and another number. You can write the definition of the term() function as follows: // Function to get the value of a term double term(char* str, int& index) { double value = 0.0; // Somewhere to accumulate // the result value = number(str, index);
// Get the first number in the term
// Loop as long as we have a good operator while((*(str + index) == ‘*’) || (*(str + index) == ‘/’)) { if(*(str + index) == ‘*’) value *= number(str, ++index);
// If it’s multiply, // multiply by next number
if(*(str + index) == ‘/’) value /= number(str, ++index);
// If it’s divide, // divide by next number
} return value; }
298
// We’ve finished, so return what // we’ve got
More about Program Structure How the Function Functions You first declare a local double variable, value, in which you’ll accumulate the value of the current term. Because a term must contain at least one number, the first action in the function is to obtain the value of the first number by calling the number() function and storing the result in value. You implicitly assume that the function number()accepts the string and an index to a position in the string as arguments, and returns the value of the number found. Because the number() function must also update the index to the string to the position after the number that was found, you’ll again specify the second parameter as a reference when you come to define that function. The rest of the term() function is a while loop that continues as long as the next character is ‘*’ or ‘/’. Within the loop, if the character found at the current position is ‘*’, you increment the variable index to position it at the beginning of the next number, call the function number() to get the value of the next number, and then multiply the contents of value by the value returned. In a similar manner, if the current character is ‘/‘, you increment the index variable and divide the contents of value by the value returned from number(). Because the function number() automatically alters the value of the variable index to the character following the number found, index is already set to select the next available character in the string on the next iteration. The loop terminates when a character other than a multiply or divide operator is found, whereupon the current value of the term accumulated in the variable value is returned to the calling program. The last analytical function that you require is number(), which determines the numerical value of any number appearing in the string.
Analyzing a Number Based on the way you have used the number() function within the term() function, you need to declare it with this prototype: double number(char* str, int& index);
// Function to recognize a number
The specification of the second parameter as a reference allows the function to update the argument in the calling program directly, which is what you require. You can make use of a function provided in a standard C++ library here. The header file provides definitions for a range of functions for testing single characters. These functions return values of type int where positive values corresponding to true and zero corresponds to false. Four of these functions are shown in the following table: Functions in the
Header for Testing Single Characters
int isalpha(int c)
Returns true if the argument is alphabetic, false otherwise.
int isupper(int c)
Returns true if the argument is an upper case letter, false otherwise.
int islower(int c)
Returns true if the argument is a lower case letter, false otherwise.
int isdigit(int c)
Returns true if the argument is a digit, false otherwise.
299
Chapter 6 There are also a number of other functions provided by , but I won’t grind through all the detail. If you’re interested, you can look them up in the MSDN Library Help. A search on “is routines” should find them. You only need the last of the functions shown above in the program. Remember that isdigit() is testing a character, such as the character ‘9’ (ASCII character 57 in decimal notation) for instance, not a numeric 9, because the input is a string. You can define the function number() as follows: // Function to recognize a number in a string double number(char* str, int& index) { double value = 0.0; // Store the resulting value while(isdigit(*(str + index))) // Loop accumulating leading digits value = 10*value + (*(str + index++) - ‘0’);
if(*(str + index) != ‘.’) return value;
// Not a digit when we get to here // so check for decimal point // and if not, return value
double factor = 1.0; // Factor for decimal places while(isdigit(*(str + (++index)))) // Loop as long as we have digits { factor *= 0.1; // Decrease factor by factor of 10 value = value + (*(str + index) - ‘0’)*factor; // Add decimal place } return value;
// On loop exit we are done
}
How the Function Functions You declare the local variable value as type double that holds the value of the number that is found. You initialize it with 0.0 because you add in the digit values as you go along. As the number in the string is a series of digits as ASCII characters, the function steps through the string accumulating the value of the number digit by digit. This occurs in two phases — the first phase accumulates digits before the decimal point; then if you find a decimal point, the second phase accumulates the digits after it. The first step is in the while loop that continues as long as the current character selected by the variable index is a digit. The value of the digit is extracted and added to the variable value in the loop statement: value = 10*value + (*(str + index++) - ‘0’);
300
More about Program Structure The way this is constructed bears a closer examination. A digit character has an ASCII value between 48, corresponding to the digit 0, and 57 corresponding to the digit 9. Thus, if you subtract the ASCII code for ‘0’ from the code for a digit, you convert it to its equivalent numeric digit value from 0 to 9. You have parentheses around the subexpression *(str + index++) - ‘0’; these are not essential, but they do make what’s going on a little clearer. The contents of the variable value are multiplied by 10 to shift the value one decimal place to the left before adding in the digit value because you’ll find digits from left to right — that is, the most significant digit first. This process is illustrated in Figure 6-6. digits in number
ASCII codes as decimal values
5
1
3
53
49
51
Intial value = 0 1st digit
value = 10*value + (53 - 48) = 10*0.0 + 5 = 5.0
2nd digit
value = 10*value + (49 - 48) = 10*5.0 + 1 = 51.0
3rd digit
value = 10*value + (51 - 48) = 10*51.0 + 3 = 513.0
Figure 6-6
As soon as you come across something other than a digit, it is either a decimal point or something else. If it’s not a decimal point, you’ve finished, so you return the current contents of the variable value to the calling program. If it is a decimal point, you accumulate the digits corresponding to the fractional part of the number in the second loop. In this loop, you use the factor variable, which has the initial value 1.0, to set the decimal place for the current digit, and consequently factor is multiplied by 0.1 for each digit found. Thus, the first digit after the decimal point is multiplied by 0.1, the second by 0.01, the third by 0.001, and so on. This process is illustrated in Figure 6-7.
301
Chapter 6 digits in integral part of number
ASCII codes as decimal values
digits in fractional part of number
5
1
3
.
6
0
8
53
49
51
46
54
49
56
Before the decimal point value = 513.0 factor = 1.0
1st digit
factor = 0.1*factor value = value + factor*(54 - 48) = 513.0 + 0.1*6 = 513.6
2nd digit
factor = 0.1*factor value = value + factor*(49 - 48) = 513.6 + 0.01*0 = 513.60
3rd digit
factor = 0.1*factor value = value + factor*(56 - 48) = 513.60 + 0.001*8 = 513.608
Figure 6-7 As soon as you find a non-digit character, you are done, so after the second loop you return the value of the variable value. You almost have the whole thing now. You just need a main() function to read the input and drive the process.
Putting the Program Together You can collect the #include statements together and assemble the function prototypes at the beginning of the program for all the functions used in this program: // Ex6_09.cpp // A program to implement a calculator
302
#include #include #include using std::cin; using std::cout; using std::endl;
// For stream input/output // For the exit() function // For the isdigit() function
void eatspaces(char* str); double expr(char* str); double term(char* str, int& index); double number(char* str, int& index);
// // // //
const int MAX = 80;
// Maximum expression length, // including ‘\0’
Function Function Function Function
to eliminate blanks evaluating an expression analyzing a term to recognize a number
More about Program Structure You have also defined a global variable MAX, which is the maximum number of characters in the expression processed by the program (including the terminating ‘\0’ character. Now you can add the definition of the main() function and your program is complete. The main() function should read a string and exit if it is empty; otherwise, call the function expr() to evaluate the input and display the result. This process should repeat indefinitely. That doesn’t sound too difficult, so let’s give it a try. int main() { char buffer[MAX] = {0}; cout << << << << <<
// Input area for expression to be evaluated
endl “Welcome to your friendly calculator.” endl “Enter an expression, or an empty line to quit.” endl;
for(;;) { cin.getline(buffer, sizeof buffer); eatspaces(buffer);
// Read an input line // Remove blanks from input
if(!buffer[0]) return 0;
// Empty line ends calculator
cout << “\t= “ << expr(buffer) << endl << endl;
// Output value of expression
} }
How the Function Functions In main(), you set up the char array buffer to accept an expression up to 80 characters long (including the string termination character). The expression is read within the indefinite for loop using the getline() input function and after obtaining the input, spaces are removed from the string by calling the function eatspaces(). All the other things that the function main() provides for are within the loop. They are to check for an empty string, which consists of just the null character, ‘\0’, in which case the program ends, and to output the value of the string produced by the function expr(). After you type all the functions, you should get output similar to the following: 2 * 35 = 70 2/3 + 3/4 + 4/5 + 5/6 + 6/7 = 3.90714 1 + 2.5 + 2.5*2.5 + 2.5*2.5*2.5 = 25.375
You can enter as many calculations as you like, and when you are fed up with it, just press Enter to end the program.
303
Chapter 6
Extending the Program Now that you have got a working calculator, you can start to think about extending it. Wouldn’t it be nice to be able to handle parentheses in an expression? It can’t be that difficult, can it? Let’s give it a try. Think about the relationship between something in parentheses that might appear in an expression and the kind of expression analysis that you have made so far. Look at an example of the kind of expression you want to handle: 2*(3 + 4) / 6 - (5 + 6) / (7 + 8)
Notice that the expressions in parentheses always form part of a term in your original parlance. Whatever sort of computation you come up with, this is always true. In fact, if you could substitute the value of the expressions within parentheses back into the original string, you would have something that you can already deal with. This indicates a possible approach to handling parentheses. You might be able to treat an expression in parentheses as just another number, and modify the function number() to sort out the value of whatever appears between the parentheses. That sounds like a good idea, but ‘sorting out’ the expression in parentheses requires a bit of thought: the clue to success is in the terminology used here. An expression that appears within parentheses is a perfectly good example of a full-blown expression, and you already have the expr() function that will return the value of an expression. If you can get the number() function to work out what the contents of the parentheses are and extract those from the string, you could pass the substring that results to the expr() function, so recursion would really simplify the problem. What’s more, you don’t need to worry about nested parentheses. Because any set of parentheses contains what you have defined as an expression, they are taken care of automatically. Recursion wins again. Take a stab at rewriting the number() function to recognize an expression between parentheses. // Function to recognize an expression in parentheses // or a number in a string double number(char* str, int& index) { double value = 0.0; // Store the resulting value if(*(str + index) == ‘(‘) { char* psubstr = 0; psubstr = extract(str, ++index); value = expr(psubstr); delete[]psubstr; return value; }
// Start of parentheses // // // // //
Pointer for substring Extract substring in brackets Get the value of the substring Clean up the free store Return substring value
while(isdigit(*(str + index))) // Loop accumulating leading digits value = 10*value + (*(str + index++) - 48); // Not a digit when we get to here if(*(str + index)!= ‘.’) // so check for decimal point return value; // and if not, return value double factor = 1.0; while(isdigit(*(str + (++index)))) {
304
// Factor for decimal places // Loop as long as we have digits
More about Program Structure factor *= 0.1; // Decrease factor by factor of 10 value = value + (*(str + index) - 48)*factor; // Add decimal place } return value;
// On loop exit we are done
}
This is not yet complete, because you still need the extract() function, but you’ll fix that in a moment.
How the Function Functions Look how little has changed to support parentheses. I suppose it is a bit of a cheat because you use a function (extract()) that you haven’t written yet, but for one extra function you get as many levels of nested parentheses as you want. This really is icing on the cake, and it’s all down to the magic of recursion! The first thing that the function number() does now is to test for a left parenthesis. If it finds one, it calls another function, extract() to extract the substring between the parentheses from the original string. The address of this new substring is stored in the pointer psubstr, so you then apply the expr() function to the substring by passing this pointer as an argument. The result is stored in value, and after releasing the memory allocated on the free store in the function extract() (as you will eventually implement it), you return the value obtained for the substring as though it were a regular number. Of course, if there is no left parenthesis to start with, the function number() continues exactly as before.
Extracting a Substring You now need to write the function extract(). It’s not difficult, but it’s also not trivial. The main complication comes from the fact that the expression within parentheses may also contain other sets of parentheses, so you can’t just go looking for the first right parenthesis you can find. You must watch out for more left parentheses as well, and for every one you find, ignore the corresponding right parenthesis. You can do this by maintaining a count of left parentheses as you go along, adding one to the count for each left parenthesis you find. If the left parenthesis count is not zero, you subtract one for each right parenthesis. Of course, if the left parenthesis count is zero and you find a right parenthesis, you’re at the end of the substring. The mechanism for extracting a parenthesized substring is illustrated in Figure 6-8. Finding ')' with the '(' count at zero indicates the end of the parenthesized substring has been reached
Signals start of substring Original expression string
'(' count:
(
2
+
3
*
(
5
-
2
)
/
2
*
(
1
1
+
9
)
0
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
1
0
copy
2
+
3
*
(
5
-
2
)
/
)
replace with '\0'
2
*
(
1
1
+
9
)
\0
Substring that was between parentheses
Figure 6-8
305
Chapter 6 Because the string you extract here contains subexpressions enclosed within parentheses, eventually extract() is called again to deal with those. The function extract()also needs to allocate memory for the substring and return a pointer to it. Of course, the index to the current position in the original string needs to end up selecting the character following the substring, so the parameter for that needs to be specified as a reference. The prototype of extract(), therefore, is as follows: char* extract(char* str, int& index); //Function to extract a substring
You can now have a shot at the definition of the function. // Function to extract a substring between parentheses // (requires header file) char* extract(char* str, int& index) { char buffer[MAX]; // Temporary space for substring char* pstr = 0; // Pointer to new string for return int numL = 0; // Count of left parentheses found int bufindex = index; // Save starting value for index do { buffer[index - bufindex] = *(str + index); switch(buffer[index - bufindex]) { case ‘)’: if(numL == 0) { buffer[index - bufindex] = ‘\0’; // Replace ‘)’ with ‘\0’ ++index; pstr = new char[index - bufindex]; if(!pstr) { cout << “Memory allocation failed,” << “ program terminated.”; exit(1); } strcpy_s(pstr, index-bufindex, buffer); // Copy substring to new memory return pstr; // Return substring in new memory } else numL--; // Reduce count of ‘(‘ to be matched break; case ‘(‘: numL++; break; }
306
// Increase count of ‘(‘ to be // matched
More about Program Structure } while(*(str + index++) != ‘\0’);
// Loop - don’t overrun end of string
cout << “Ran off the end of the expression, must be bad input.” << endl; exit(1); return pstr; }
How the Function Functions You declare a char array to temporarily hold the substring. You don’t know how long the substring will be, but it can’t be more than MAX characters. You can’t return the address of buffer to the calling function because it is local and will be destroyed on exit from the function; therefore, you need to allocate some memory on the free store when you know how long the string is. You do this by declaring a variable pstr of type ‘pointer to char’, which you return by value when you have the substring safe and sound in the free store memory. You declare a counter numL to keep track of left parentheses in the substring (as I discussed earlier). The initial value of index (when the function begins execution) is stored in the variable bufindex. You use this in combination with incremented values of index to index the array buffer. The executable part of the function is basically one big do-while loop. The substring is copied from str to buffer one character on each loop iteration, with a check for left or right parentheses during each cycle. If a left parenthesis is found, numL is incremented, and if a right parenthesis is found and numL is non-zero, it is decremented. When you find a right parenthesis and numL is zero, you have found the end of the substring. The ‘)’ in the substring in buffer is then replaced by ‘\0’, and sufficient memory is obtained on the free store to hold the substring. The substring in buffer is then copied to the memory you obtained through the operator new by using the strcpy_s() function that is declared in the header file; this is a safe version of the old strcpy() function that is declared in the same header. This function copies the string specified by the third argument, buffer, to the address specified by the first argument, pstr. The second argument is the length of the destination string, pstr. If you fall through the bottom of the loop, it means that you hit the ‘\0’ at the end of the expression in str without finding the complementary right bracket, so you display a message and terminate the program.
Running the Modified Program After replacing the number() function in the old version of the program, adding the #include statement for , and incorporating the prototype and the definition for the new extract() function you have just written, you’re ready to roll with an all-singing, all-dancing calculator. If you have assembled all that without error, you can get output something like this: Welcome to your friendly calculator. Enter an expression, or an empty line to quit. 1/(1+1/(1+1/(1+1))) = 0.6 (1/2-1/3)*(1/3-1/4)*(1/4-1/5) = 0.000694444 3.5*(1.25-3/(1.333-2.1*1.6))-1
307
Chapter 6 = 8.55507 2,4-3.4 Arrrgh!*#!! There’s an error
The friendly and informative error message in the last output line is due to the use of the comma instead of the decimal point in the expression above it, in what should be 2.4. As you can see, you get nested parentheses to any depth with a relatively simple extension of the program, all due to the amazing power of recursion.
C++/CLI Programming Just about everything discussed so far in relation to functions for native C++ applies equally well to C++/CLI language code with the proviso that parameter types and return types will be fundamental types, which as you know are equivalent to value class types in a CLR program, tracking handle types, or tracking reference types. Because you cannot perform arithmetic on the address stored in a tracking handle, the coding techniques that I demonstrated for treating parameters that are native C++ arrays as pointers on which you can perform arithmetic operations do not apply to C++/CLI arrays. Many of the complications that can arise with arguments to native C++ functions disappear, but there is still the odd trap in C++/CLI for the unwary. A CLR version of the calculator can help you understand how functions look written in C++/CLI. The throw and catch mechanism for exceptions works the same in CLR programs as it does in native C++ programs, but there are some differences. The exceptions that you throw in a C++/CLI program must always be thrown using tracking handles. Consequently you should always be throwing exception objects and as far as possible you should avoid throwing literals, especially string literals. For example, consider this try-catch code: try { throw L”Catch me if you can.”; } catch(String^ ex) // The exception will not be caught by this { Console::WriteLine(L”String^: {0}”,ex); }
The catch block cannot catch the object thrown here, because the throw statement throws an exception of type const wchar_t*, not of type String^. To catch the exception as thrown, the catch block needs to be: try { throw L”Catch me if you can.”; } catch(const wchar_t* ex) // OK. The exception thrown is of this type { String^ exc = gcnew String(ex); Console::WriteLine(L”wchar_t:{0}”, exc); }
308
More about Program Structure This catch block catches the exception because it now has the correct type. To throw the exception so that it can be caught by the original catch block, you must change the code in the try block: try { throw gcnew String(L”Catch me if you can.”); } catch(String^ ex) // OK. Exception thrown is of this type { Console::WriteLine(L”String^: {0}”,ex); }
Now the exception is a String object and is thrown as type String^, a handle that references the string. You can use function templates in your C++/CLI programs, but you have an additional capability called generic functions that looks remarkably similar; however, there are significant differences.
Understanding Generic Functions Although generic functions appear to do the same thing as function templates and therefore at first sight seem superfluous, generic functions work rather differently from template functions and the differences make them a valuable additional capability in CLR programs. When you use a function template, the compiler generates the source code for functions that you require from the template; this generated code is then compiled along with the rest of your program code. In some cases this can result in many functions being generated and the size of the execution module may be increased substantially. On the other hand a generic function specification is itself compiled, and when you call a function that matches the generic function specification, actual types are substituted for the type parameters at execution time. No extra code is generated at compile time and the code-bloat that can arise with template functions does not occur. Some aspects of defining generic function depend on a knowledge of stuff that comes in later chapters but it will al be clear eventually. I’ll provide minimal explanations here of the things that are new and you’ll learn the detail later in the book.
Defining Generic Functions You define a generic function using type parameters that are replaced by actual types when the function is called. Here’s an example of a generic function definition: generic where T:IComparable T MaxElement(array^ x) { T max = x[0]; for(int i = 1; i < x->Length; i++) if(max->CompareTo(x[i]) < 0) max = x[i]; return max; }
309
Chapter 6 This generic function does the same job as the native C++ function template you saw earlier in this chapter. The generic keyword in the first line identifies what follows as a generic specification and the first line defines the type parameter for the function as T; the typename keyword between the angled brackets indicates that the T that follows is the name of a type parameter in the generic function, and that this type parameter is replaced by an actual type when the generic function is used. For a generic function with multiple type parameters, the parameter names go between the angled brackets, each preceded by the typename keyword and separated by commas. The where keyword that follows the closing angled bracket introduces a constraint on the actual type that may be substituted for T when the generic function is used. This particular constraint says that any type that is to replace T in the generic function must implement the IComparable interface. You’ll learn about interfaces later in the book, but for now I’ll say that it implies that the type must define the CompareTo() function that allows two objects of the type to be compared. Without this constraint the compiler has no knowledge of what operations are possible for the type that is to replace T because until the generic function is used, this is completely unknown. With the constraint you can use the CompareTo() function to compare max with an element of the array. The CompareTo() function returns an integer value that is less than 0 when the object for which it is called (max in this case) is less than the argument, zero if it equals the argument, and greater than 0 if it is greater than the argument. The second line specifies the generic function name MaxElement, its return type T, and its parameter list. This looks rather like an ordinary function header except that it involves the generic type parameter T. The return type for the generic function and the array element type that is part of the parameter type specification are both of type T, so both these types are determined when the generic function is used.
Using Generic Functions The simplest way of calling a generic function is just to use it like any ordinary function. For example, you could use the generic MaxElement() from the previous section like this: array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; double maxData = MaxElement(data);
The compiler is able to deduce that the type argument to the generic function is double in this instance and generates the code to call the function accordingly. The function executes with instances of T in the function being replaced by double. As I said earlier, this is not like a template function; there is no creation of function instances at compile time. The compiled generic function is able to handle type argument substitutions when it is called. Note that if you pass a string literal as a argument to a generic function, the compiler deduces that the type argument is String^, regardless of whether the string literal is a narrow string constant such as “Hello!” or a wide string constant such as L”Hello!”. It is possible that the compiler may not be able to deduce the type argument from a call of a generic function. In these instances you can specify the type argument(s) explicitly between angled brackets following the function name in the call. For example, you could write the call in the previous fragment as: double maxData = MaxElement<double>(data);
With an explicit type argument specified there is no possibility of ambiguity.
310
More about Program Structure There are limitations on what types you can supply as a type argument to a generic function. A type argument cannot be a native C++ class type, or a native pointer or reference, or a handle to a value class type such as int^. Thus only value class types such as int or double and tracking handles such as String^ are allowed (but not a handle to a value class type). Let’s try a working example.
Try It Out
Using a Generic Function
Here’s an example that defines and uses three generic functions: // Ex6_10A.cpp : main project file. // Defining and using generic functions #include “stdafx.h” using namespace System; // Generic function to find the maximum element in an array generic where T:IComparable T MaxElement(array^ x) { T max = x[0]; for(int i = 1; i < x->Length; i++) if(max->CompareTo(x[i]) < 0) max = x[i]; return max; } // Generic function to remove an element from an array generic where T:IComparable array^ RemoveElement(T element, array^ data) { array^ newData = gcnew array(data->Length - 1); int index = 0; // Index to elements in newData array bool found = false; // Indicates that the element to remove was found for each(T item in data) { // Check for invalid index or element found if((!found) && item->CompareTo(element) == 0) { found = true; continue; } else { if(index == newData->Length) { Console::WriteLine(L”Element to remove not found”); return data; } newData[index++] = item;
311
Chapter 6 } } return newData; } // Generic function to list an array generic where T:IComparable void ListElements(array^ data) { for each(T item in data) Console::Write(L”{0,10}”, item); Console::WriteLine(); } int main(array<System::String ^> ^args) { array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; Console::WriteLine(L”Array contains:”); ListElements(data); Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(data)); array<double>^ result = RemoveElement(MaxElement(data), data); Console::WriteLine(L” After removing maximum, array contains:”); ListElements(result); array^ numbers = {3, 12, 7, 0, 10,11}; Console::WriteLine(L”\nArray contains:”); ListElements(numbers); Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(numbers)); Console::WriteLine(L”\nAfter removing maximum, array contains:”); ListElements(RemoveElement(MaxElement(numbers), numbers)); array<String^>^ strings = {L”Many”, L”hands”, L”make”, L”light”, L”work”}; Console::WriteLine(L”\nArray contains:”); ListElements(strings); Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(strings)); Console::WriteLine(L”\nAfter removing maximum, array contains:”); ListElements(RemoveElement(MaxElement(strings), strings)); return 0; }
The output from this example is: Array contains: 1.5
3.5
6.7
4.2
Maximum element = 6.7 After removing maximum, array contains: 1.5 3.5 4.2 2.1 Array contains:
312
2.1
More about Program Structure 3
12
7
0
10
After removing maximum, array contains: 3 7 0 10
11
11
Maximum element = 12
Array contains: Many hands
make
light
work
Maximum element = work
After removing maximum, array contains: Many hands make light Press any key to continue . . .
How It Works The first generic function that this example defines is MaxElement(), which is identical to the one you saw in the preceding section that finds the maximum element in an array so I won’t discuss it again. The next generic function, RemoveElements(), removes the element passed as the first argument from the array specified by the second argument and the function returns a handle to the new array that results from the operation. You can see from the first two lines of the function definition that both parameter types and the return type involve the type parameter, T. generic where T:IComparable array^ RemoveElement(T element, array^ data)
The constraint on T is the same as for the first generic function and implies that whatever type is used as the type argument must implement the CompareTo() function to allow objects of the type to be compared. The second parameter and the return type are both handles to an array of elements of type T. The first parameter is simply type T. The function first creates an array to hold the result: array^ newData = gcnew array(data->Length - 1);
The newData array is of the same type as the second argument(an array of elements of type T(but has one fewer elements because one element is to be removed from the original. The elements are copied from the data array to the newData array in the for each loop: int index = 0; // Index to elements in newData array bool found = false; // Indicates that element to remove was found for each(T item in data) { // Check for invalid index or element found if((!found) && item->CompareTo(element) == 0)
313
Chapter 6 { found = true; continue; } else { if(index == newData->Length) { Console::WriteLine(L”Element to remove not found”); return data; } newData[index++] = item; } }
All elements are copied except the one identified by the first argument to the function. You use the index variable to select the next newData array element that is to receive the next element from the data array. Each element from data is copied unless it is equal to element, in which case found is set to true and the continue statement skips to the next iteration. It is quite possible that an array could have more than one element equal to the first argument and the found variable prevents subsequent elements that are the same as element from being skipped in the loop. You also have a check for the index variable exceeding the legal limit for indexing elements in newData. This could arise if there is no element in the data array equal to the first argument to the function. In this eventuality, you just return the handle to the original array. The third generic function just lists the elements from an array of type array: generic void ListElements(array^ data) { for each(T item in data) Console::Write(L”{0,10}”, item); Console::WriteLine(); }
This is one of the few occasions where no constraint on the type parameter is needed. There is very little you can do with objects that are of a completely unknown type so typically generic function type parameters will have constraints. The operation of the function is very simple — each element from the array is written to the command line in an output field with a width of 10 in the for each loop. If you want, you could add a little sophistication by adding a parameter for the field width and creating the format string to be used as the first argument to the Write() function in the Console class. You could also add logic to the loop to write a specific number of elements per line based on the field width. The main() function exercises these generic functions with type parameters of types double, int, and String^. Thus you can see that all three generic functions work with value types and handles. In the second and third examples, the generic functions are used in combination in a single statement. For example, look at this statement in the third sample use of the functions: ListElements(RemoveElement(MaxElement(strings), strings));
The first argument to the RemoveElement() generic function is produced by the MaxElement() generic function call, so these generic function can be used in the same way as equivalent ordinary functions.
314
More about Program Structure The compiler is able to deduce the type argument in all instances where the generic functions are used but you could specify the explicitly if you wanted to. For example, you could write the previous statement like this: ListElements(RemoveElement<String^>(MaxElement<String^>(strings), strings));
A Calculator Program for the CLR Let’s re-implement the calculator as a C++/CLI example. We’ll assume the same program structure and hierarchy of functions as you saw for the native C++ program but the functions will be declared and defined as C++/CLI functions. A good starting point is the set of function prototypes at the beginning of the source file(I have used Ex6_11 as the CLR project name: // Ex6_11.cpp : main project file. // A CLR calculator supporting parentheses #include “stdafx.h” #include
// For exit()
using namespace System; String^ eatspaces(String^ str); double expr(String^ str); double term(String^ str, int^ index); double number(String^ str, int^ index); String^ extract(String^ str, int^ index);
// // // // //
Function Function Function Function Function
to eliminate blanks evaluating an expression analyzing a term to recognize a number to extract a substring
All the parameters are now handles; the string parameters are type String^ and the index parameter that records the current position in the string is also a handle of type int^. Of course, a string is returned as a handle of type String^. The main() function implementation looks like this: int main(array<System::String ^> ^args) { String^ buffer; // Input area for expression to be evaluated
Console::WriteLine(L”Welcome to your friendly calculator.”); Console::WriteLine(L”Enter an expression, or an empty line to quit.”); for(;;) { buffer = eatspaces(Console::ReadLine()); if(String::IsNullOrEmpty(buffer)) return 0; Console::WriteLine(L” } return 0;
// Read an input line // Empty line ends calculator
= {0}\n\n”,expr(buffer)); // Output value of expression
}
315
Chapter 6 The function is a lot shorter and easier to read. Within the indefinite for loop you call the String class function, IsNullOrEmpty(). This function returns true if the string passed as the argument is null or of zero length so it does exactly what you want here.
Removing Spaces from the Input String The function to remove spaces is also simpler and shorter: // Function to eliminate spaces from a string String^ eatspaces(String^ str) { // Array to hold string without spaces array<wchar_t>^ chars = gcnew array<wchar_t>(str->Length); int length = 0; // Number of chars in array // Copy non-space characters to chars array for each(wchar_t ch in str) if(ch != ‘ ‘) chars[length++] = ch; // Return chars array as string return gcnew String(chars, 0, length); }
You first create an array to accommodate the string when the spaces have been removed. This is an array of elements of type wchar_t because strings in C++/CLI are Unicode characters. The process for removing the spaces is very simple — you copy all the characters that are not spaces from the string str to the array, chars, keeping track of the number of characters copied in the length variable. You finally create a new String object using a String class constructor that creates the object from elements in an array. The first argument to the constructor is the array that is the source of characters for the string, the second argument is the index position for the first character from the array that forms the string, and the third argument is the total number of characters from the array that are to be used. The String class defines a range of constructors for creating strings in various ways.
Evaluating an Arithmetic Expression You can implement the function to evaluate an expression like this: // Function to evaluate an arithmetic expression double expr(String^ str) { int^ index = 0; // Keeps track of current character position double value = term(str, index); while(*index < str->Length) { switch(str[*index]) { case ‘+’: ++(*index); value += term(str, index);
316
// Get first term
// Choose action based on current character // + found so // increment index and add // the next term
More about Program Structure break; case ‘-’: ++(*index); value -= term(str, index); break;
// - found so // decrement index and add // the next term
default: // If we reach here the string is junk Console::WriteLine(L”Arrrgh!*#!! There’s an error.\n”); exit(1); } } return value; }
The index variable is declared as a handle because you want to pass it to the term() function and allow the term() function to modify the original variable. If you declared index simply as type int, the term() function would receive a copy of the value and could not refer to the original variable to change it. The declaration of index results in a warning message from the compiler because the statement relies on autoboxing of the value 0 to produce the Int32 value class object that the handle references and the warning is because people often write the statement like this but intend to initialize the handle to null. Of course, to do this you must use nullptr as the initial value in place of 0. If you want to eliminate the warning, you could rewrite the statement as: int^ index = gcnew int(0);
This statement uses a constructor explicitly to create the object and initialize it with 0 so there’s no warning from the compiler. After processing the initial term by calling the term() function the while loop steps through the string looking for + or - operators followed by another term. The switch statement identifies and processes the operators. If you have been using native C++ for a while, you might be tempted to write the case statements in the switch a little differently(for example: // Incorrect code!! Does not work!! case ‘+’: value += term(str, ++(*index)); break;
// + found so // increment index & add the next term
Of course, this would typically be written without the first comment. This code is wrong, but why is it wrong? The term() function expects a handle in type int^ as the second argument and that is what is supplied here although maybe not what you expect. The compiler arranges for the expression ++(*index) to be evaluated and the result stored in a temporary location. The expression indeed updates the value referenced by index, but the handle that is passed to the term() function is a handle to the temporary location holding the result of evaluating the expression, not the handle index. This handle is produced by autoboxing the value stored in the temporary location. When the term() function updates, the value referenced by the handle that is passed to it the temporary location is updated, not the location referenced by index; thus all the updates to the index position of the string made in the term() function are lost. If you expect a function to update a variable in the calling program, you must not use an expression as the function argument — you must always use the handle variable name.
317
Chapter 6 Obtaining the Value of a Term As in the native C++ version, the term() function steps through the string that is passed as the first argument, starting at the character position referenced by the second argument: // Function to get the value of a term double term(String^ str, int^ index) { double value = number(str, index);
// Get the first number in the term
// Loop as long as we have characters and a good operator while(*index < str->Length) { if(str[*index] == L’*’) // If it’s multiply, { ++(*index); // increment index and value *= number(str, index); // multiply by next number } else if( str[*index] == L’/’) // If it’s divide { ++(*index); // increment index and value /= number(str, index); // divide by next number } else break; // Exit the loop } // We’ve finished, so return what we’ve got return value; }
After calling the number() function to get the value of the first number or parenthesized expression in a term, the function steps through the string in the while loop. The loop continues while there are still characters in the input string as long as a * or / operators followed by another number or parenthesized expression is found.
Evaluating a Number The number function extracts and evaluates a parenthesized expression if there is one; otherwise, it determines the value of the next number in the input: // Function to recognize a number double number(String^ str, int^ index) { double value = 0.0;
// Store for the resulting value
// Check for expression between parentheses if(str[*index] == L’(‘ ) // Start of parentheses { ++(*index); String^ substr = extract(str, index); // Extract substring in brackets return expr(substr); // Return substring value } // Loop accumulating leading digits
318
More about Program Structure while((*index < str->Length) && Char::IsDigit(str, *index)) { value = 10.0*value + Char::GetNumericValue(str[(*index)]); ++(*index); } // Not a digit when we get to here if((*index == str->Length) || str[*index] != ‘.’) return value; double factor = 1.0; ++(*index);
// so check for decimal point // and if not, return value
// Factor for decimal places // Move to digit
// Loop as long as we have digits while((*index < str->Length) && Char::IsDigit(str, *index)) { factor *= 0.1; // Decrease factor by factor of 10 value = value + Char::GetNumericValue(str[*index])*factor; // Add decimal place ++(*index); } return value;
// On loop exit we are done
}
As in the native C++ version, the extract() function is used to extract a parenthesized expression and the substring that results is passed to the expr() function to be evaluated. If there’s no parenthesized expression(indicated by the absence of an opening parenthesis, the input is scanned for a number, which is a sequence of zero or more digits followed by an optional decimal point plus fractional digits. The IsDigit() function in the Char class returns true if a character is a digit and false otherwise. The character here is in the string passed as the first argument to the function at the index position specified by the second argument. There’s another version if the IsDigit() function that accepts a single argument of type wchar_t so you could use this with the argument str[*index]. The GetNumericValue() function in the Char class returns the value of a Unicode digit character that you pass as the argument as a value of type double. There’s another version of this function to which you can pass a string handle and an index position to specify the character.
Extracting a Parenthesized Substring You can implement the extract() function that returns a parenthesized substring from the input like this: // Function to extract a substring between parentheses String^ extract(String^ str, int^ index) { // Temporary space for substring array<wchar_t>^ buffer = gcnew array<wchar_t>(str->Length); String^ substr; // Substring to return int numL = 0; // Count of left parentheses found int bufindex = *index; // Save starting value for index while(*index < str->Length) { buffer[*index - bufindex] = str[*index]; switch(str[*index])
319
Chapter 6 { case ‘)’: if(numL == 0) { array<wchar_t>^ substrChars = gcnew array<wchar_t>(*index - bufindex); str->CopyTo(bufindex, substrChars, 0, substrChars->Length); substr = gcnew String(substrChars); ++(*index); return substr; } else numL--; break; case ‘(‘: numL++;
// Return substring in new memory
// Reduce count of ‘(‘ to be matched
// Increase count of ‘(‘ to be // matched
break; } ++(*index); } Console::WriteLine(L”Ran off the end of the expression, must be bad input.”); exit(1); return substr; }
Again, the strategy is the same as the native C++ version, but the differences are in the detail. To find the complementary right parenthesis the function keeps track of how many new left parentheses are found using the variable numL. The substring is extracted when a right parenthesis is found and the left parenthesis count in numL is zero. The substring is copied into the substrChars array using the CopyTo() function for the String object, str. The function copies characters beginning at the string index position specified by the first argument into the array specified by the second argument; the third argument defined the starting element in the array to receive characters and the fourth argument is the number of characters to be copied. You create the string that is returned as the result of the extraction by using the String class constructor that creates an object from all the elements in the array, substrChars, that is passed as the argument. If you assemble all the functions in a CLR console project you’ll have a C++/CLI implementation of the calculator running with the CLR. The output should be much the same as the native C++ version.
Summar y You now have a reasonably comprehensive knowledge of writing and using functions. You’ve used a pointer to a function in a practical context for handling out-of-memory conditions in the free store, and you have used overloading to implement a set of functions providing the same operation with different types of parameters. You’ll see more about overloading functions in the following chapters.
320
More about Program Structure The important bits that you learned in this chapter include: ❑
A pointer to a function stores the address of a function, plus information about the number and types of parameters and return type for a function.
❑
You can use a pointer to a function to store the address of any function with the appropriate return type, and number and types of parameters.
❑
You can use a pointer to a function to call the function at the address it contains. You can also pass a pointer to a function as a function argument.
❑
An exception is a way of signaling an error in a program so that the error handling code can be separated from the code for normal operations.
❑
You throw an exception with a statement that uses the keyword throw.
❑
Code that may throw exceptions should be placed in a try block, and the code to handle a particular type of exception is placed in a catch block immediately following the try block. There can be several catch blocks following a try block, each catching a different type of exception.
❑
Overloaded functions are functions with the same name, but with different parameter lists.
❑
When you call an overloaded function, the function to be called is selected by the compiler based on the number and types of the arguments that you specify.
❑
A function template is a recipe for generating overloaded functions automatically.
❑
A function template has one or more arguments that are type variables. An instance of the function template — that is, a function definition — is created by the compiler for each function call that corresponds to a unique set of type arguments for the template.
❑
You can force the compiler to create a particular instance from a function template by specifying the function you want in a prototype declaration.
You also got some experience of using several functions in a program by working through the calculator example. But remember that all the uses of functions up to now have been in the context of a traditional procedural approach to programming. When we come to look at object-oriented programming, you will still use functions extensively, but with a very different approach to program structure and to the design of a solution to a problem.
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
Consider the following function: int ascVal(size_t i, const char* p) { // print the ASCII value of the char if (!p || i > strlen(p)) return -1; else return p[i]; }
321
Chapter 6 Write a program that will call this function through a pointer and verify that it works. You’ll need an #include directive for the header in your program to use the strlen() function.
2.
Write a family of overloaded functions called equal(), which take two arguments of the same type, returning 1 if the arguments are equal, and 0 otherwise. Provide versions having char, int, double, and char* arguments. (Use the strcmp() function from the runtime library to test for equality of strings. If you don’t know how to use strcmp(), search for it in the online help. You’ll need an #include directive for the header file in your program.) Write test code to verify that the correct versions are called.
3.
At present, when the calculator hits an invalid input character, it prints an error message, but doesn’t show you where the error was in the line. Write an error routine that prints out the input string, putting a caret (^) below the offending character, like this:
12 + 4,2*3 ^
4.
Add an exponentiation operator, ^, to the calculator, fitting it in alongside * and /. What are the limitations of implementing it in this way, and how can you overcome them?
5.
(Advanced) Extend the calculator so it can handle trig and other math functions, allowing you to input expressions such as:
2 * sin(0.6)
The math library functions all work in radians; provide versions of the trigonometric functions so that the user can use degrees, for example: 2 * sind(30)
322
7 Defining Your Own Data Types This chapter is about creating your own data types to suit your particular problem. It’s also about creating objects, the building blocks of object-oriented programming. An object can seem a bit mysterious to the uninitiated but, as you will see in this chapter, an object can be just an instance of one of your own data types. In this chapter, you will learn about: ❑
Structures and how they are used
❑
Classes and how they are used
❑
The basic components of a class and how you define class types
❑
Creating and using objects of a class
❑
Controlling access to members of a class
❑
Constructors and how to create them
❑
The default constructor
❑
References in the context of classes
❑
The copy constructor and how it is implemented
❑
How C++/CLI classes differ from native C++ classes
❑
Properties in a C++/CLI class and how you define and use them
❑
Literal fields and how you define and use them
❑
initonly fields and how you define and use them
❑
What a static constructor is
Chapter 7
The struct in C++ A structure is a user-defined type that you define using the keyword struct so it is often referred as a struct. The struct originated back in the C language, and C++ incorporates and expands on the C struct. A struct in C++ is functionally replaceable by a class insofar as anything you can do with a struct you can also achieve by using a class; however, because Windows was written in C before C++ became widely used, the struct appears pervasively in Windows programming. It is also widely used today, so you really need to know something about structs. We’ll first take a look at (C-style) structs in this chapter before exploring the more extensive capabilities offered by classes.
What Is a struct? Almost all the variables that you have seen up to now have been able to store a single type of entity — a number of some kind, a character, or an array of elements of the same type. The real world is a bit more complicated than that, and just about any physical object you can think of needs several items of data to describe it even minimally. Think about the information that might be needed to describe something as simple as a book. You might consider title, author, publisher, date of publication, number of pages, price, topic or classification, and ISBN number just for starters, and you can probably come up with a few more without too much difficulty. You could specify separate variables to contain each of the parameters that you need to describe a book, but ideally you would want to have a single data type, BOOK say, which embodied all of these parameters. I’m sure you won’t be surprised to hear that this is exactly what a struct can do for you.
Defining a struct Let’s stick with the notion of a book, and suppose that you just want to include the title, author, publisher, and year of publication within your definition of a book. You could declare a structure to accommodate this as follows: struct BOOK { char Title[80]; char Author[80]; char Publisher[80]; int Year; };
This doesn’t define any variables, but it actually creates a new type for variables and the name of the type is BOOK. The keyword struct defines BOOK as such, and the elements making up an object of this type are defined within the braces. Note that each line defining an element in the struct is terminated by a semicolon, and that a semicolon also appears after the closing brace. The elements of a struct can be of any type, except the same type as the struct being defined. You couldn’t have an element of type BOOK included in the structure definition for BOOK, for example. You may think this to be a limitation, but note that you could include a pointer to a variable of type BOOK, as you’ll see a little later on. The elements Title, Author, Publisher, and Year enclosed between the braces in the definition above may also be referred to as members or fields of the BOOK structure. Each object of type BOOK contains the members Title, Author, Publisher, and Year. You can now create variables of type BOOK in exactly the same way that you create variables of any other type:
324
Defining Your Own Data Types BOOK Novel;
// Declare variable Novel of type BOOK
This declares a variable with the name Novel that you can now use to store information about a book. All you need now is to understand how you get data into the various members that make up a variable of type BOOK.
Initializing a struct The first way to get data into the members of a struct is to define initial values in the declaration. Suppose you wanted to initialize the variable Novel to contain the data for one of your favorite books, Paneless Programming, published in 1981 by the Gutter Press. This is a story of a guy performing heroic code development while living in an igloo, and as you probably know, inspired the famous Hollywood box office success, Gone with the Window. It was written by I.C. Fingers, who is also the author of that seminal three-volume work, The Connoisseur’s Guide to the Paper Clip. With this wealth of information you can write the declaration for the variable Novel as: BOOK Novel = { “Paneless Programming”, “I.C. Fingers”, “Gutter Press”, 1981 };
// // // //
Initial Initial Initial Initial
value value value value
for for for for
Title Author Publisher Year
The initializing values appear between braces, separated by commas, in much the same way that you defined initial values for members of an array. As with arrays, the sequence of initial values obviously needs to be the same as the sequence of the members of the struct in its definition. Each member of the structure Novel has the corresponding value assigned to it, as indicated in the comments.
Accessing the Members of a struct To access individual members of a struct, you can use the member selection operator, which is a period; this is sometimes referred to as the member access operator. To refer to a particular member, you write the struct variable name, followed by a period, followed by the name of the member that you want to access. To change the Year member of the Novel structure, you could write: Novel.Year = 1988;
This would set the value of the Year member to 1988. You can use a member of a structure in exactly the same way as any other variable of the same type as the member. To increment the member Year by two, for example, you can write: Novel.Year += 2;
This increments the value of the Year member of the struct, just like any other variable.
325
Chapter 7 Try It Out
Using structs
Now use another console application example to exercise a little further how referencing the members of a struct works. Suppose you want to write a program to deal with some of the things you might find in a yard, such as those illustrated in the professionally landscaped yard in Figure 7-1.
House
10
Position 0,0
Hut 25
90
80
40
30
70
30
110
Pool 70
10
Hut 25
Position 100,120
Figure 7-1
I have arbitrarily assigned the coordinates 0,0 to the top-left corner of the yard. The bottom-right corner has the coordinates 100,120. Thus, the first coordinate value is a measure of the horizontal position relative to the top-left corner, with values increasing from left to right, and the second coordinate is a measure of the vertical position from the same reference point, with values increasing from top to bottom.
326
Defining Your Own Data Types Figure 7-1 also shows the position of the pool and that of the two huts relative to the top-left corner of the yard. Because the yard, huts, and pool are all rectangular, you could define a struct type to represent any of these objects: struct RECTANGLE { int Left; int Top; int Right; int Bottom;
// Top left point // coordinate pair // Bottom right point // coordinate pair
};
The first two members of the RECTANGLE structure type correspond to the coordinates of the top-left point of a rectangle, and the next two to the coordinates of the bottom-right point. You can use this in an elementary example dealing with the objects in the yard as follows: // Ex7_01.cpp // Exercising structures in the yard #include using std::cout; using std::endl; // Definition of a struct to represent rectangles struct RECTANGLE { int Left; // Top left point int Top; // coordinate pair int Right; int Bottom;
// Bottom right point // coordinate pair
}; // Prototype of function to calculate the area of a rectangle long Area(RECTANGLE& aRect); // Prototype of a function to move a rectangle void MoveRect(RECTANGLE& aRect, int x, int y); int main(void) { RECTANGLE Yard = { 0, 0, 100, 120 }; RECTANGLE Pool = { 30, 40, 70, 80 }; RECTANGLE Hut1, Hut2; Hut1.Left = 70; Hut1.Top = 10; Hut1.Right = Hut1.Left + 25; Hut1.Bottom = 30; Hut2 = Hut1; MoveRect(Hut2, 10, 90);
// Define Hut2 the same as Hut1 // Now move it to the right position
cout << endl
327
Chapter 7 << “Coordinates of Hut2 are “ << Hut2.Left << “,” << Hut2.Top << “ and “ << Hut2.Right << “,” << Hut2.Bottom; cout << endl << “The area of the yard is “ << Area(Yard); cout << << << <<
endl “The area of the pool is “ Area(Pool) endl;
return 0; } // Function to calculate the area of a rectangle long Area(RECTANGLE& aRect) { return (aRect.Right - aRect.Left)*(aRect.Bottom - aRect.Top); } // Function to Move a Rectangle void MoveRect(RECTANGLE& aRect, int x, int y) { int length = aRect.Right - aRect.Left; // Get length of rectangle int width = aRect.Bottom - aRect.Top; // Get width of rectangle aRect.Left = x; aRect.Top = y; aRect.Right = x + length; aRect.Bottom = y + width;
// // // //
Set top left point to new position Get bottom right point as increment from new position
return; }
The output from this example is: Coordinates of Hut2 are 10,90 and 35,110 The area of the yard is 12000 The area of the pool is 1600
How It Works Note that the struct definition appears at global scope in this example. You’ll be able to see it in the Class View tab for the project. Putting the definition of the struct at global scope allows you to declare a variable of type RECTANGLE anywhere in the .cpp file. In a program with a more significant amount of code, such definitions would normally be stored in a .h file and then added to each .cpp file where necessary by using a #include directive. You have defined two functions to process RECTANGLE objects. The function Area() calculates the area of the RECTANGLE object that you pass as a reference argument as the product of the length and the width, where the length is the difference between the horizontal positions of the defining points, and the
328
Defining Your Own Data Types width is the difference between the vertical positions of the defining points. By passing a reference, the code runs a little faster because the argument is not copied. The MoveRect() function modifies the defining points of a RECTANGLE object to position it at the coordinates x, y which are passed as arguments. The position of a RECTANGLE object is assumed to be the position of the Left, Top point. Because the RECTANGLE object is passed as a reference, the function is able to modify the members of the RECTANGLE object directly. After calculating the length and width of the RECTANGLE object passed, the Left and Top members are set to x and y respectively, and the new Right and Bottom members are calculated by incrementing x and y by the length and width of the original RECTANGLE object. In the main() function, you initialize the Yard and Pool RECTANGLE variables with their coordinate positions as shown in Figure 7-1. The variable Hut1 represents the hut at the top-right in the illustration and its members are set to the appropriate values using assignment statements. The variable Hut2, corresponding to the hut at the bottom-left of the yard, is first set to be the same as Hut1 in the assignment statement Hut2 = Hut1;
// Define Hut2 the same as Hut1
This statement results in the values of the members of Hut1 being copied to the corresponding members of Hut2. You can only assign a struct of a given type to another of the same type. You can’t increment a struct directly or use a struct in an arithmetic expression. To alter the position of Hut2 to its place at the bottom-left of the yard, you call the MoveRect() function with the coordinates of the required position as arguments. This roundabout way of getting the coordinates of Hut2 is totally unnecessary and serves only to show how you can use a struct as an argument to a function.
Intellisense Assistance with Structures You’ve probably noticed that the editor in Visual C++ 2005 is quite intelligent — it knows the types of variables, for instance. If you hover the mouse cursor over a variable name in the editor window, it pop ups a little box showing its definition. It also can help a lot with structures (and classes, as you will see) because not only does it know the types of ordinary variables, it also knows the members that belong to a variable of a particular structure type. If your computer is reasonably fast, as you type the member selection operator following a structure variable name, the editor pops a window showing the list of members. If you click one of the members, it shows the comment that appeared in the original definition of the structure, so you know what it is. This is shown in Figure 7-2 using a fragment of the previous example.
Figure 7-2
329
Chapter 7 Now there’s a real incentive to add comments, and to keep them short and to the point. If you doubleclick on a member in the list or press the Enter key when the item is highlighted, it is automatically inserted after the member selection operator, thus eliminating one source of typos in your code. Great, isn’t it? You can turn any or all of the Intellisense features off if you want to via the Tools > Options menu item, but I guess the only reason you would want to is if your machine is too slow to make them useful. You can turn the statement-completion features on or off on the C/C++ editor page that you select in the right options pane. If you turn them off, you can still call them up when you want too, either through the Edit menu or through the keyboard. Pressing Ctrl+J, for example, pops up the members for an object under the cursor. The editor also shows the parameter list for a function when you are typing the code to call it — it pops up as soon as you enter the left parenthesis for the argument list. This is particularly helpful with library functions as its tough to remember the parameter list for all of them. Of course, the #include directive for the header file must already be there in the source code for this to work. Without it the editor has no idea what the library function is. You will see more things that the editor can help with as you learn more about classes. After that interesting little diversion, let’s get back to structures.
The struct RECT Rectangles are used a great deal in Windows programs. For this reason, there is a RECT structure predefined in the header file windows.h. Its definition is essentially the same as the structure that you defined in the last example: struct RECT { int left; int top; int right; int bottom;
// Top left point // coordinate pair // Bottom right point // coordinate pair
};
This struct is usually used to define rectangular areas on your display for a variety of purposes. Because RECT is used so extensively, windows.h also contains prototypes for a number of functions to manipulate and modify rectangles. For example, windows.h provides the function InflateRect() to increase the size of a rectangle and the function EqualRect() to compare two rectangles. MFC also defines a class called CRect, which is the equivalent of a RECT structure. After you understand classes, you will be using this in preference to the RECT structure. The CRect class provides a very extensive range of functions for manipulating rectangles, and you will be using a number of these when you are writing Windows programs using MFC.
Using Pointers with a struct As you might expect, you can create a pointer to a variable of a structure type. In fact, many of the functions declared in windows.h that work with RECT objects require pointers to a RECT as arguments
330
Defining Your Own Data Types because this avoids the copying of the whole structure when a RECT argument is passed to a function. To define a pointer to a RECT object for example, the declaration is what you might expect: RECT* pRect = NULL;
// Define a pointer to RECT
Assuming that you have defined a RECT object, aRect, you can set the pointer to the address of this variable in the normal way, using the address-of operator: pRect = &aRect;
// Set pointer to the address of aRect
As you saw when the idea of a struct was introduced, a struct can’t contain a member of the same type as the struct being defined, but it can contain a pointer to a struct, including a pointer to a struct of the same type. For example, you could define a structure like this: struct ListElement { RECT aRect; ListElement* pNext; };
// RECT member of structure // Pointer to a list element
The first element of the ListElement structure is of type RECT, and the second element is a pointer to a structure of type ListElement — the same type as that being defined. (Remember that this element isn’t of type ListElement, it’s of type ‘pointer to ListElement’.) This allows object of type ListElement to be daisy-chained together, where each ListElement can contain the address of the next ListElement object in a chain, the last in the chain having the pointer as zero. This is illustrated in Figure 7-3. LE1
LE2
LE3
members: aRect pnext = &LE2
members: aRect pnext = &LE3
members: aRect pnext = &LE4
LE4
LE5
members: aRect pnext = &LE5
members: aRect pnext = 0
No next element
Figure 7-3
Each box in the diagram represents an object of type ListElement and the pNext member of each object stores the address of the next object in the chain, except for the last object where pNext is 0. This kind of arrangement is usually referred to as a linked list. It has the advantage that as long as you know the first element in the list, you can find all the others. This is particularly important when variables are created dynamically, since a linked list can be used to keep track of them all. Every time a new one is created, it’s simply added to the end of the list by storing its address in the pNext member of the last object in the chain.
331
Chapter 7 Accessing Structure Members through a Pointer Consider the following statements: RECT aRect = { 0, 0, 100, 100 }; RECT* pRect = &aRect;
The first declares and defines the aRect object to be of type RECT with the first pair of members initialized to (0, 0) and the second pair to (100, 100). The second statement declares pRect as a pointer to type RECT and initializes it with the address of aRect. You can now access the members of aRect through the pointer with a statement such as this: (*pRect).Top += 10;
// Increment the Top member by 10
The parentheses to de-reference the pointer here are essential because the member access operator takes precedence over the de-referencing operator. Without the parentheses, you would be attempting to treat the pointer as a struct and to de-reference the member, so the statement would not compile. After executing this statement, the Top member will have the value 10 and, of course, the remaining members will be unchanged. The method that you used here to access the member of a struct through a pointer looks rather clumsy. Because this kind of operation crops up very frequently in C++, the language includes a special operator to enable you to express the same thing in a much more readable and intuitive form, so let’s look at that next.
The Indirect Member Selection Operator The indirect member selection operator, ->, is specifically for accessing members of a struct through a pointer; this operator is also referred to as the indirect member access operator. The operator looks like a little arrow (->)and is formed from a minus sign (-) followed by the symbol for greater than (>) You could use it to rewrite the statement to access the Top member of aRect through the pointer pRect, as follows: pRect->Top += 10;
// Increment the Top member by 10
This is much more expressive of what is going on, isn’t it? The indirect member selection operator is also used with classes, and you’ll be seeing a lot more of it throughout the rest of the book.
Data Types, Objects, Classes and Instances Before I get into the language, syntax, and programming techniques of classes, I’ll start by considering how your existing knowledge relates to the concept of classes. So far, you’ve learned that native C++ lets you create variables that can be any of a range of fundamental data types: int, long, double and so on. You have also seen how you can use the struct keyword to define a structure that you could then use as the type for a variable representing a composite of several other variables.
332
Defining Your Own Data Types The variables of the fundamental types don’t allow you to model real-world objects (or even imaginary objects) adequately. It’s hard to model a box in terms of an int, for example; however, you can use the members of a struct to define a set of attributes for such an object. You could define variables, length, width, and height to represent the dimensions of the box and bind them together as members of a Box structure, as follows: struct Box { double length; double width; double height; };
With this definition of a new data type called Box, you define variables of this type just as you did with variables of the basic types. You can then create, manipulate, and destroy as many Box objects as you need to in your program. This means that you can model objects using structs and write your programs around them. So — that’s object-oriented programming all wrapped up then? Well, not quite. You see, object-oriented programming (OOP) is based on a number of foundations (famously encapsulation, polymorphism, and inheritance) and what you have seen so far doesn’t quite fit the bill. Don’t worry about what these terms mean for the moment — you’ll be exploring that in the rest of this chapter and throughout the book. The notion of a struct in C++ goes far beyond the original concept of struct in C — it incorporates the object-oriented notion of a class. This idea of classes, from which you can create your own data types and use them just like the native types, is fundamental to C++, and the new keyword class was introduced into the language to describe this concept. The keywords struct and class are almost identical in C++, except for the access control to the members, which you will find out more about later in this chapter. The keyword struct is maintained for backwards compatibility with C, but everything that you can do with a struct and more, you can achieve with a class. Take a look at how you might define a class representing boxes: class CBox { public: double m_Length; double m_Width; double m_Height; };
When you define CBox as a class you are essentially defining a new data type, similar to when you defined the Box structure. The only differences here are the use of the keyword class instead of struct, and the use of the keyword public followed by a colon that precedes the definition of the members of the class. The variables that you define as part of the class are called data members of the class, because they are variables that store data. You have also called the class CBox instead of Box. You could have called the class Box, but the MFC adopts the convention of using the prefix C for all class names, so you might as well get used to it. MFC also prefixes data members of classes with m_ to distinguish them from other variables, so I’ll use this
333
Chapter 7 convention too. Remember though that in other contexts where you might use C++ and in C++/CLI in particular, this will not be the case; in some instances the convention for naming classes and their members may be different, and in others there may be no particular convention adopted for naming entities. The public keyword is a clue as to the difference between a structure and a class. It just defines the members of the class as being generally accessible, in the same way as the members of a structure are. The members of a struct, however, are public by default. As you’ll see a little later in the chapter, though, it’s also possible to place restrictions on the accessibility of the class members. You can declare a variable, bigBox say, that represents an instance of the CBox class type like this: CBox bigBox;
This is exactly the same as declaring a variable for a struct, or indeed for any other variable type. After you have defined the class CBox, declarations for variables of this type are quite standard.
First Class The notion of class was invented by an Englishman to keep the general population happy. It derives from the theory that people who knew their place and function in society would be much more secure and comfortable in life than those who did not. The famous Dane, Bjarne Stroustrup, who invented C++, undoubtedly acquired a deep knowledge of class concepts while at Cambridge University in England and appropriated the idea very successfully for use in his new language. Class in C++ is similar to the English concept, in that each class usually has a very precise role and a permitted set of actions. However, it differs from the English idea because class in C++ has largely socialist overtones, concentrating on the importance of working classes. Indeed, in some ways it is the reverse of the English ideal, because, as you will see, working classes in C++ often live on the backs of classes that do nothing at all.
Operations on Classes In C++ you can create new data types as classes to represent whatever kinds of objects you like. As you’ll come to see, classes (and structures) aren’t limited to just holding data; you can also define member functions or even operations that act between objects of your classes using the standard C++ operators. You can define the class CBox, for example, so that the following statements work and have the meanings you want them to have: CBox box1; CBox box2; if(box1 > box2) box1.fill(); else box2.fill();
// Fill the larger box
You could also implement operations as part of the CBox class for adding, subtracting or even multiplying boxes — in fact, almost any operation to which you could ascribe a sensible meaning in the context of boxes.
334
Defining Your Own Data Types I’m talking about incredibly powerful medicine here and it constitutes a major change in the approach that you can take to programming. Instead of breaking down a problem in terms of what are essentially computer-related data types (integer numbers, floating point numbers and so on) and then writing a program, you’re going to be programming in terms of problem-related data types, in other words classes. These classes might be named CEmployee, or CCowboy, or CCheese, or CChutney, each defined specifically for the kind of problem that you want to solve, complete with the functions and operators that are necessary to manipulate instances of your new types. Program design now starts with deciding what new application-specific data types you need to solve the problem in hand and writing the program in terms of operations on the specifics that the problem is concerned with, be it CCoffins or CCowpokes.
Terminology I’ll first summarize some of the terminology that I will be using when discussing classes in C++: ❑
A class is a user-defined data type.
❑
Object-oriented programming (OOP) is the programming style based on the idea of defining your own data types as classes.
❑
Declaring an object of a class type is sometimes referred to as instantiation because you are creating an instance of a class.
❑
Instances of a class are referred to as objects.
❑
The idea of an object containing the data implicit in its definition, together with the functions that operate on that data, is referred to as encapsulation.
When I get into the detail of object-oriented programming, it may seem a little complicated in places, but getting back to the basics of what you’re doing can often help to make things clearer, so always keep in mind what objects are really about. They are about writing programs in terms of the objects that are specific to the domain of your problem. All the facilities around classes in C++ are there to make this as comprehensive and flexible as possible. Let’s get down to the business of understanding classes.
Understanding Classes A class is specification of a data type that you define. It can contain data elements that can either be variables of the basic types in C++, or of other user-defined types. The data elements of a class may be single data elements, arrays, pointers, arrays of pointers of almost any kind, or objects of other classes, so you have a lot of flexibility in what you can include in your data type. A class also can contain functions that operate on objects of the class by accessing the data elements that they include. So, a class combines both the definition of the elementary data that makes up an object and the means of manipulating the data that belongs to individual objects of the class. The data and functions within a class are called members of the class. Humorously enough, the members of a class that are data items are called data members and the members that are functions are called function members or member functions. The member functions of a class are also sometimes referred to as methods; I will not use this term in this book but keep it in mind as you may see it used elsewhere.
335
Chapter 7 The data members are also referred to as fields, and this terminology is used with C++/CLI, so I will be using this terminology from time to time. When you define a class, you define a blueprint for a data type. This doesn’t actually define any data but it does define what the class name means, that is, what an object of the class will consist of and what operations can be performed on such an object. It’s much the same as if you wrote a description of the basic type double. This wouldn’t be an actual variable of type double, but a definition of how it’s made up and how it operates. To create a variable of a basic data type, you need to use a declaration statement. It’s exactly the same with classes, as you will see.
Defining a Class Take a look again at the class example mentioned earlier — a class of boxes. You defined the CBox data type using the keyword class as follows: class CBox { public: double m_Length; double m_Width; double m_Height; };
// Length of a box in inches // Width of a box in inches // Height of a box in inches
The name of the class appears following the class keyword, and the three data members are defined between the curly braces. The data members are defined for the class using the declaration statements that you already know and love and the whole class definition is terminated with a semicolon. The names of all the members of a class are local to the class. You can therefore use the same names elsewhere in a program without causing any problems.
Access Control in a Class The public keyword looks a bit like a label, but in fact it is more than that. It determines the access attributes of the members of the class that follow it. Specifying the data members as public means that these members of an object of the class can be accessed anywhere within the scope of the class object to which they belong. You can also specify the members of a class as private or protected. In fact, if you omit the access specification altogether, the members have the default attribute, private. (This is the only difference between a class and a struct in C++ — the default access specifier for a struct is public.) You will be looking into the effect of these keywords in a class definition a bit later. Remember that all you have defined so far is a class, which is a data type. You haven’t declared any objects of the class type. When I talk about accessing a class member, say m_Height, I’m talking about accessing the data member of a particular object, and that object needs to be declared somewhere.
Declaring Objects of a Class You declare objects of a class with exactly the same sort of declaration that you use to declare objects of basic types, so you could declare objects of the CBox class type with these statements: CBox box1; CBox box2;
336
// Declare box1 of type CBox // Declare box2 of type CBox
Defining Your Own Data Types Both of the objects box1 and box2 will, of course, have their own data members. This is illustrated in Figure 7-4. box1
box2
m_Length
m_Width
m_Height
m_Length
m_Width
m_Height
8 bytes
8 bytes
8 bytes
8 bytes
8 bytes
8 bytes
Figure 7-4
The object name box1 embodies the whole object, including its three data members. They are not initialized to anything, however — the data members of each object will simply contain junk values, so we need to look at how you can access them for the purpose of setting them to some specific values.
Accessing the Data Members of a Class You can refer to the data members of objects of a class using the direct member selection operator that you used to access members of a struct. So, to set the value of the data member m_Height of the object box2 to, say, 18.0, you could write this assignment statement: box2.m_Height = 18.0;
// Setting the value of a data member
You can only access the data member in this way in a function that is outside the class because the m_Height member was specified as having public access. If it wasn’t defined as public, this statement would not compile. You’ll see more about this shortly.
Try It Out
Your First Use of Classes
Verify that you can use your class in the same way as the structure. Try it out in the following console application: // Ex7_02.cpp // Creating and using boxes #include using std::cout; using std::endl; class CBox { public: double m_Length; double m_Width; double m_Height; }; int main() { CBox box1;
// Class definition at global scope
// Length of a box in inches // Width of a box in inches // Height of a box in inches
// Declare box1 of type CBox
337
Chapter 7 CBox box2;
// Declare box2 of type CBox
double boxVolume = 0.0;
// Stores the volume of a box
box1.m_Height = 18.0; box1.m_Length = 78.0; box1.m_Width = 24.0;
// Define the values // of the members of // the object box1
box2.m_Height = box1.m_Height - 10; box2.m_Length = box1.m_Length/2.0; box2.m_Width = 0.25*box1.m_Length;
// Define box2 // members in // terms of box1
// Calculate volume of box1 boxVolume = box1.m_Height*box1.m_Length*box1.m_Width; cout << endl << “Volume of box1 = “ << boxVolume; cout << << << <<
endl “box2 has sides which total “ box2.m_Height+ box2.m_Length+ box2.m_Width “ inches.”;
cout << endl << “A CBox object occupies “ << sizeof box1 << “ bytes.”;
// Display the size of a box in memory
cout <<endl; return 0; }
As you type in the code for main(), you should see the editor prompting you with a list of member names whenever you enter a member selection operator following the name of a class object. You can then select the member you want from the list by double-clicking it. Hovering the mouse cursor for a moment over any of the variables in your code will result in the type being displayed.
How It Works Back to console application examples again for the moment, so the project should be defined accordingly. Everything here works as you would have expected from your experience with structures. The definition of the class appears outside of the function main() and, therefore, has global scope. This enables you to declare objects in any function in the program and causes the class to show up in the Class View tab once the program has been compiled. You have declared two objects of type CBox within the function main(), box1 and box2. Of course, as with variables of the basic types, the objects box1 and box2 are local to main(). Objects of a class type obey the same rules with respect to scope as variables declared as one of the basic types (such as the variable boxVolume used in this example). The first three assignment statements set the values of the data members of box1. You define the values of the data members of box2 in terms of the data members of box1 in the next three assignment statements.
338
Defining Your Own Data Types You then have a statement that calculates the volume of box1 as the product of its three data members, and this value is output to the screen. Next, you output the sum of the data members of box2 by writing the expression for the sum of the data members directly in the output statement. The final action in the program is to output the number of bytes occupied by box1, which is produced by the operator sizeof. If you run this program, you should get this output: Volume of box1 = 33696 box2 has sides which total 66.5 inches. A CBox object occupies 24 bytes.
The last line shows that the object box1 occupies 24 bytes of memory, which is a result of having 3 data members of 8 bytes each. The statement that produced the last line of output could equally well have been written like this: cout << endl << “A CBox object occupies “ << sizeof (CBox) << “ bytes.”;
// Display the size of a box in memory
Here, I have used the type name between parentheses, rather than a specific object name as the operand for the sizeof operator. You’ll remember that this is standard syntax for the sizeof operator, as you saw in Chapter 4. This example has demonstrated the mechanism for accessing the public data members of a class. It also shows that they can be used in exactly the same way as ordinary variables. You are now ready to break new ground by taking a look at member functions of a class.
Member Functions of a Class A member function of a class is a function that has its definition or its prototype within the class definition. It operates on any object of the class of which it is a member, and has access to all the members of a class for that object.
Try It Out
Adding a Member Function to CBox
To see how you access the members of the class from within a function member, create an example extending the CBox class to include a member function that calculates the volume of the CBox object. // Ex7_03.cpp // Calculating the volume of a box with a member function #include using std::cout; using std::endl; class CBox { public: double m_Length; double m_Width; double m_Height;
// Class definition at global scope
// Length of a box in inches // Width of a box in inches // Height of a box in inches
339
Chapter 7 // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } }; int main() { CBox box1; CBox box2;
// Declare box1 of type CBox // Declare box2 of type CBox
double boxVolume = 0.0;
// Stores the volume of a box
box1.m_Height = 18.0; box1.m_Length = 78.0; box1.m_Width = 24.0;
// Define the values // of the members of // the object box1
box2.m_Height = box1.m_Height - 10; box2.m_Length = box1.m_Length/2.0; box2.m_Width = 0.25*box1.m_Length;
// Define box2 // members in // terms of box1
boxVolume = box1.Volume(); // Calculate volume of box1 cout << endl << “Volume of box1 = “ << boxVolume; cout << endl << “Volume of box2 = “ << box2.Volume(); cout << endl << “A CBox object occupies “ << sizeof box1 << “ bytes.”; cout << endl; return 0; }
How It Works The new code that you add to the CBox class definition is shaded. It’s just the definition of the Volume() function, which is a member function of the class. It also has the same access attribute as the data members: public. This is because every class member that you declare following an access attribute will have that access attribute, until another one is specified within the class definition. The Volume() function returns the volume of a CBox object as a value of type double. The expression in the return statement is just the product of the three data members of the class. There’s no need to qualify the names of the class members in any way when you accessing them in member functions. The unqualified member names automatically refer to the members of the object that is current when the member function is executed.
340
Defining Your Own Data Types The member function Volume() is used in the highlighted statements in main(), after initializing the data members (as in the first example). Using the same name for a variable in main() causes no conflict or problem. You can call a member function of a particular object by writing the name of the object to be processed, followed by a period, followed by the member function name. As noted previously, the function automatically accesses the data members of the object for which it was called, so the first use of Volume() calculates the volume of box1. Using only the name of a member will always refer to the member of the object for which the member function has been called. The member function is used a second time directly in the output statement to produce the volume of box2. If you execute this example, it produces this output: Volume of box1 = 33696 Volume of box2 = 6084 A CBox object occupies 24 bytes.
Note that the CBox object is still the same number of bytes. Adding a function member to a class doesn’t affect the size of the objects. Obviously, a member function has to be stored in memory somewhere, but there’s only one copy regardless of how many class objects you create, and the memory occupied by member functions isn’t counted when the sizeof operator produces the number of bytes that an object occupies. The names of the class data members in the member function automatically refer to the data members of the specific object used to call the function, and the function can only be called for a particular object of the class. In this case, this is done by using the direct member access operator with the name of an object. If you try to call a member function without specifying an object name, your program will not compile.
Positioning a Member Function Definition A member function definition need not be placed inside the class definition. If you want to put it outside the class definition, you need to put the prototype for the function inside the class. If we rewrite the previous class with the function definition outside, the class definition looks like this: class CBox { public: double double double double };
// Class definition at global scope
m_Length; m_Width; m_Height; Volume(void);
// // // //
Length of a box in inches Width of a box in inches Height of a box in inches Member function prototype
Now you need to write the function definition, but because it appears outside the definition of the class, there has to be some way of telling the compiler that the function belongs to the class CBox. This is done by prefixing the function name with the name of the class and separating the two with the scope resolution operator, ::, which is formed from two successive colons. The function definition would now look like this: // Function to calculate the volume of a box double CBox::Volume() { return m_Length*m_Width*m_Height; }
341
Chapter 7 It produces the same output as the last example; however, it isn’t exactly the same program. In the second case, all calls to the function are treated in the way that you’re already familiar with. However, when you define a function within the definition of the class as in Ex7_03.cpp, the compiler implicitly treated the function as an inline function.
Inline Functions With an inline function, the compiler tries to expand the code in the body of the function in place of a call to the function. This avoids much of the overhead of calling the function and, therefore, speeds up your code. This is illustrated in Figure 7-5.
int main(void)
Function declared as inline in a class
{ ...
inline void function() { body }
function();
The compiler replaces calls of inline funtion with body code for the function, suitably adjusted to avoid problems with variable names or scope.
{ body } ... function(); { body } .... }
Figure 7-5
Of course, the compiler ensures that expanding a function inline doesn’t cause any problems with variable names or scope. The compiler may not always be able to insert the code for a function inline (such as with recursive functions or functions for which you have obtained an address), but generally it will work. It’s best used for very short, simple functions, such as our function Volume() in the CBox class because such functions execute faster and inserting the body code does not significantly increase the size of the executable module. With the function definition outside of the class definition, the compiler treats the function as a normal function and a call of the function will work in the usual way; however, it’s also possible to tell the compiler that, if possible, you would like the function to be considered as inline. This is done by simply placing the keyword inline at the beginning of the function header. So, for this function, the definition would be as follows: // Function to calculate the volume of a box inline double CBox::Volume() { return m_Length*m_Width*m_Height; }
342
Defining Your Own Data Types With this definition for the function, the program would be exactly the same as the original. This enables you to put the member function definitions outside of the class definition, if you so choose, and still retain the execution performance benefits of inlining. You can apply the keyword inline to ordinary functions in your program that have nothing to do with classes and get the same effect. Remember, however, that it’s best used for short, simple functions. You now need to understand a little more about what happens when you declare an object of a class.
Class Constructors In the previous program example, you declared the CBox objects, box1 and box2, and then laboriously worked through each of the data members for each object in order to assign an initial value to it. This is unsatisfactory from several points of view. First of all, it would be easy to overlook initializing a data member, particularly with a class which had many more data members than our CBox class. Initializing the data members of several objects of a complex class could involve pages of assignment statements. The final constraint on this approach arises when you get to defining data members of a class that don’t have the attribute public — you won’t be able to access them from outside the class anyway. There has to be a better way, and of course there is — it’s known as the class constructor.
What Is a Constructor? A class constructor is a special function in a class which is called when a new object of the class is created. It therefore provides the opportunity to initialize objects as they are created and to ensure that data members only contain valid values. A class may have several constructors enabling you to create objects in various ways. You have no leeway in naming the constructors in a class — they always have the same name as the class in which they are defined. The function CBox(), for example, is a constructor for our class CBox. It also has no return type. It’s wrong to specify a return type for a constructor; you must not even write it as void. The primary purpose of a class constructor is to assign initial values to the data elements of the class, and no return type for a constructor is necessary or permitted.
Try It Out
Adding a Constructor to the CBox class
Let’s extend our CBox class to incorporate a constructor. // Ex7_04.cpp // Using a constructor #include using std::cout; using std::endl; class CBox { public: double m_Length; double m_Width; double m_Height;
// Class definition at global scope
// Length of a box in inches // Width of a box in inches // Height of a box in inches
343
Chapter 7 // Constructor definition CBox(double lv, double bv, double hv) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length* m_Width* m_Height; } }; int main() { CBox box1(78.0,24.0,18.0); CBox cigarBox(8.0,5.0,1.0); double boxVolume = 0.0;
// Declare and initialize box1 // Declare and initialize cigarBox // Stores the volume of a box
boxVolume = box1.Volume(); // Calculate volume of box1 cout << endl << “Volume of box1 = “ << boxVolume; cout << endl << “Volume of cigarBox = “ << cigarBox.Volume(); cout << endl; return 0; }
How It Works The constructor CBox() has been written with three parameters of type double, corresponding to the initial values for the m_Length, m_Width and m_Height members of a CBox object. The first statement in the constructor outputs a message so that you can tell when it’s been called. You wouldn’t do this in production programs, but because it’s very helpful in showing when a constructor is called, it’s often used when testing a program. I’ll use it regularly for the purposes of illustration. The code in the body of the constructor is very simple. It just assigns the arguments that you pass to the constructor when you call it to the corresponding data members. If necessary, you could also include checks that valid, non-negative arguments are supplied and, in a real context, you probably would want to do this, but our primary interest here is in seeing how the mechanism works. Within main(), you declare the object box1 with initializing values for the data members m_Length, m_Width, and m_Height, in sequence. These are in parentheses following the object name. This uses the functional notation for initialization that, as you saw in Chapter 2, can also be applied to initializing ordinary variables of basic types. You also declare a second object of type CBox, called cigarBox, which also has initializing values.
344
Defining Your Own Data Types The volume of box1 is calculated using the member function Volume() as in the previous example and is then displayed on the screen. You also display the value of the volume of cigarBox. The output from the example is: Constructor called. Constructor called. Volume of box1 = 33696 Volume of cigarBox = 40
The first two lines are output from the two calls of the constructor CBox(), once for each object declared. The constructor that you have supplied in the class definition is automatically called when a CBox object is declared, so both CBox objects are initialized with the initializing values appearing in the declaration. These are passed to the constructor as arguments, in the sequence that they are written in the declaration. As you can see, the volume of box1 is the same as before and cigarBox has a volume looking suspiciously like the product of its dimensions, which is quite a relief.
The Default Constructor Try modifying the last example by adding the declaration for box2 that we had previously: CBox box2;
// Declare box2 of type CBox
Here, we’ve left box2 without initializing values. When you rebuild this version of the program, you get the error message: error C2512: ‘CBox’: no appropriate default constructor available
This means that the compiler is looking for a default constructor for box2 (also referred to as the no-arg constructor because it doesn’t require arguments when it is called) because you haven’t supplied any initializing values for the data members. A default constructor is one that does not require any arguments to be supplied, which can be either a constructor that has no parameters specified in the constructor definition, or one whose arguments are all optional. Well, this statement was perfectly satisfactory in Ex7_02.cpp, so why doesn’t it work now? The answer is that the previous example used a default no-argument constructor that was supplied by the compiler, and the compiler provided this constructor because you didn’t supply one. Because in this example you did supply a constructor, the compiler assumed that you were taking care of everything and didn’t supply the default. So, if you still want to use declarations for CBox objects that aren’t initialized, you have to include the default constructor yourself. What exactly does the default constructor look like? In the simplest case, it’s just a constructor that accepts no arguments; it doesn’t even need to do anything: CBox() {}
// Default constructor // Totally devoid of statements
You can see such a constructor in action.
345
Chapter 7 Try It Out
Supplying a Default Constructor
Let’s add our version of the default constructor to the last example, along with the declaration for box2, plus the original assignments for the data members of box2. You must enlarge the default constructor just enough to show that it is called. Here is the next version of the program: // Ex7_05.cpp // Supplying and using a default constructor #include using std::cout; using std::endl; class CBox { public: double m_Length; double m_Width; double m_Height;
// Class definition at global scope
// Length of a box in inches // Width of a box in inches // Height of a box in inches
// Constructor definition CBox(double lv, double bv, double hv) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Default constructor definition CBox() { cout << endl << “Default constructor called.”; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } }; int main() {
346
CBox box1(78.0,24.0,18.0); CBox box2; CBox cigarBox(8.0, 5.0, 1.0);
// Declare and initialize box1 // Declare box2 - no initial values // Declare and initialize cigarBox
double boxVolume = 0.0;
// Stores the volume of a box
boxVolume = box1.Volume(); cout << endl
// Calculate volume of box1
Defining Your Own Data Types << “Volume of box1 = “ << boxVolume; box2.m_Height = box1.m_Height - 10; // Define box2 box2.m_Length = box1.m_Length / 2.0; // members in box2.m_Width = 0.25*box1.m_Length; // terms of box1 cout << endl << “Volume of box2 = “ << box2.Volume(); cout << endl << “Volume of cigarBox = “ << cigarBox.Volume(); cout << endl; return 0; }
How It Works Now that you have included your own version of the default constructor, there are no error messages from the compiler and everything works. The program produces this output: Constructor called. Default constructor called. Constructor called. Volume of box1 = 33696 Volume of box2 = 6084 Volume of cigarBox = 40
All that the default constructor does is display a message. Evidently, it was called when you declared the object box2. You also get the correct value for the volumes of all three CBox objects, so the rest of the program is working as it should. One aspect of this example that you may have noticed is that you now know we can overload constructors just as you overloaded functions in Chapter 6. You have just executed an example with two constructors that differ only in their parameter list. One has three parameters of type double and the other has no parameters at all.
Assigning Default Parameter Values in a Class When discussing functions, you saw how you could specify default values for the parameters to a function in the function prototype. You can also do this for class member functions, including constructors. If you put the definition of the member function inside the class definition, you can put the default values for the parameters in the function header. If you include only the prototype of a function in the class definition, the default parameter values should go in the prototype. If you decided that the default size for a CBox object was a unit box with all sides of length 1, you could alter the class definition in the last example to this: class CBox { public:
// Class definition at global scope
347
Chapter 7 double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
// Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Default constructor definition CBox() { cout << endl << “Default constructor called.”; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } };
If you make this change to the last example, what happens? You get another error message from the compiler, of course. Amongst a lot of other stuff, you get these useful comments from the compiler: warning C4520: ‘CBox’: multiple default constructors specified error C2668: ‘CBox::CBox’: ambiguous call to overloaded function
This means that the compiler can’t work out which of the two constructors to call — the one for which you have set default values for the parameters or the constructor that doesn’t accept any parameters. This is because the declaration of box2 requires a constructor without parameters and either constructor can now be called without parameters. The immediately obvious solution to this is to get rid of the constructor that accepts no parameters. This is actually beneficial. Without this constructor, any CBox object declared without being explicitly initialized will automatically have its members initialized to 1.
Try It Out
Supplying Default Values for Constructor Arguments
You can demonstrate this with the following simplified example: // Ex7_06.cpp // Supplying default values for constructor arguments #include using std::cout; using std::endl; class CBox { public: double m_Length;
348
// Class definition at global scope
// Length of a box in inches
Defining Your Own Data Types double m_Width; double m_Height;
// Width of a box in inches // Height of a box in inches
// Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } }; int main() { CBox box2;
// Declare box2 - no initial values
cout << endl << “Volume of box2 = “ << box2.Volume(); cout << endl; return 0; }
How It Works You only declare a single uninitialized CBox variable — box2 — because that’s all you need for demonstration purposes. This version of the program produces the following output: Constructor called. Volume of box2 = 1
This shows that the constructor with default parameter values is doing its job of setting the values of objects that have no initializing values specified. You should not assume from this that this is the only, or even the recommended, way of implementing the default constructor. There will be many occasions where you won’t want to assign default values in this way, in which case you’ll need to write a separate default constructor. There will even be times when you don’t want to have a default constructor operating at all, even though you have defined another constructor. This would ensure that all declared objects of a class must have initializing values explicitly specified in their declaration.
349
Chapter 7
Using an Initialization List in a Constructor Previously, you initialized the members of an object in the class constructor using explicit assignment. You could also have used a different technique, using what is called an initialization list. I can demonstrate this with an alternative version of the constructor for the class CBox: // Constructor definition using an initialization list CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0): m_Length(lv), m_Width(bv), m_Height(hv) { cout << endl << “Constructor called.”; }
The way this constructor definition is written assumes that it appears within the body of the class definition. Now the values of the data members are not set in assignment statements in the body of the constructor. As in a declaration, they are specified as initializing values using functional notation and appear in the initializing list as part of the function header. The member m_Length is initialized by the value of lv, for example. This can be more efficient than using assignments as you did in the previous version. If you substitute this version of the constructor in the previous example, you will see that it works just as well. Note that the initializing list for the constructor is separated from the parameter list by a colon and each of the initializers is separated by a comma. This technique for initializing parameters in a constructor is important, because, as you will see later, it’s the only way of setting values for certain types of data members of an object. The MFC also rely heavily on the initialization list technique.
Private Members of a Class Having a constructor that sets the values of the data members of a class object but still admits the possibility of any part of a program being able to mess with what are essentially the guts of an object is almost a contradiction in terms. To draw an analogy, after you have arranged for a brilliant surgeon such as Dr. Kildare, whose skills were honed over years of training, to do things to your insides, letting the local plumber or bricklayer have a go hardly seems appropriate. You need some protection for your class data members. You can get the security you need by using the keyword private when you define the class members. Class members that are private can, in general, be accessed only by member functions of a class. There’s one exception, but we’ll worry about that later. A normal function has no direct means of accessing the private members of a class. This is shown in Figure 7-6. Having the possibility of specifying class members as private also enables you to separate the interface to the class from its internal implementation. The interface to a class is composed of the public members and the public member functions in particular because they can provide indirect access to all the members of a class, including the private members. By keeping the internals of a class private, you can later modify them to improve performance for example without necessitating modifications to the code that uses the class through its public interface. To keep data and function members of a class safe from unnecessary meddling, it’s good practice to declare those that don’t need to be exposed as private. Only make public what is essential to the use of your class.
350
Defining Your Own Data Types Class Object
public Data Members
OK
public Function Members
No
private Data Members
Normal Function that is not in the Class
Error No Access Error
No
private Function Members
Free access to all members by any member function
OK
Figure 7-6
Try It Out
Private Data Members
You can rewrite the CBox class to make its data members private. // Ex7_07.cpp // A class with private members #include using std::cout; using std::endl; class CBox { public:
// Class definition at global scope
// Constructor definition using an initialisation list CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0): m_Length(lv), m_Width(bv), m_Height(hv) { cout << endl << “Constructor called.”; } // Function to calculate the volume of a box double Volume()
351
Chapter 7 { return m_Length*m_Width*m_Height; } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main() { CBox match(2.2, 1.1, 0.5); CBox box2;
// Declare match box // Declare box2 - no initial values
cout << endl << “Volume of match = “ << match.Volume(); // Uncomment the following line to get an error // box2.m_Length = 4.0; cout << endl << “Volume of box2 = “ << box2.Volume(); cout << endl; return 0; }
How It Works The definition of the CBox class now has two sections. The first is the public section containing the constructor and the member function Volume(). The second section is specified as private and contains the data members. Now the data members can only be accessed by the member functions of the class. You don’t have to modify any of the member functions — they can access all the data members of the class anyway. If you uncomment the statement in the function main() that assigns a value to the m_Length member of the object box2, however, you’ll get a compiler error message confirming that the data member is inaccessible. If you haven’t already done so, take a look at the members of the CBox class in Class View; you’ll see that the icon alongside each member indicates its accessibility; a small padlock in the icon shows when a member is private. A point to remember is that using a constructor or a member function is now the only way to get a value into a private data member of an object. You have to make sure that all the ways in which you might want to set or modify private data members of a class are provided for through member functions. You can also put functions into the private section of a class. In this case, they can only be called by other function members of the same class. If you put the function Volume() in the private section, you will get a compiler error from the statements that attempt to use it in the function main(). If you put the constructor in the private section, you won’t be able to declare any objects of the class type.
352
Defining Your Own Data Types The preceding example generates this output: Constructor called. Constructor called. Volume of match = 1.21 Volume of box2 = 1
The output demonstrates that the class is still working satisfactorily, with its data members defined as having the access attribute private. The major difference is that they are now completely protected from unauthorized access and modification. If you don’t specify otherwise, the default access attribute that applies to members of a class is private. You could, therefore, put all your private members at the beginning of the class definition
and let them default to private by omitting the keyword. It’s better, however, to take the trouble to explicitly state the access attribute in every case, so there can be no doubt about what you intend. Of course, you don’t have to make all your data members private. If the application for your class requires it, you can have some data members defined as private and some as public. It all depends on what you’re trying to do. If there’s no reason to make members of a class public, it is better to make them private as it makes the class more secure. Ordinary functions won’t be able to access any of the private members of your class.
Accessing private Class Members On reflection, declaring the data members of a class as private is rather extreme. It’s all very well protecting them from unauthorized modification, but that’s no reason to keep their values a secret. What you need is a Freedom of Information Act for private members. You don’t have to start writing to your state senator to get it — it’s already available to you. All that’s necessary is to write a member function to return the value of a data member. Look at this member function for the class CBox: inline double CBox::GetLength() { return m_Length; }
Just to show how it looks, this has been written as a member function definition which is external to the class. I’ve specified it as inline, since we’ll benefit from the speed increase without increasing the size of the code too much. Assuming that you have the declaration of the function in the public section of the class, you can use it by writing statements such as this: int len = box2.GetLength();
// Obtain data member length
All you need to do is to write a similar function for each data member that you want to make available to the outside world, and their values can be accessed without prejudicing the security of the class. Of course, if you put the definitions for these functions within the class definition, they will be inline by default.
353
Chapter 7
The friend Functions of a Class There may be circumstances when, for one reason or another, you want certain selected functions which are not members of a class to, nonetheless, be able to access all the members of a class — a sort of elite group with special privileges. Such functions are called friend functions of a class and are defined using the keyword friend. You can either include the prototype of a friend function in the class definition, or you can include the whole function definition. Functions that are friends of a class and are defined within the class definition are also by default inline. Friend functions are not members of the class, and therefore the access attributes do not apply to them. They are just ordinary global functions with special privileges. Imagine that you want to implement a friend function in the CBox class to compute the surface area of a CBox object.
Try It Out
Using a friend to Calculate the Surface Area
You can see how a friend function works in the following example: // Ex7_08.cpp // Creating a friend function of a class #include using std::cout; using std::endl; class CBox { public:
// Class definition at global scope
// Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } private: double m_Length; double m_Width; double m_Height; // Friend function friend double BoxSurface(CBox aBox);
354
// Length of a box in inches // Width of a box in inches // Height of a box in inches
Defining Your Own Data Types }; // friend function to calculate the surface area of a Box object double BoxSurface(CBox aBox) { return 2.0*(aBox.m_Length*aBox.m_Width + aBox.m_Length*aBox.m_Height + aBox.m_Height*aBox.m_Width); } int main() { CBox match(2.2, 1.1, 0.5); CBox box2;
// Declare match box // Declare box2 - no initial values
cout << endl << “Volume of match = “ << match.Volume(); cout << endl << “Surface area of match = “ << BoxSurface(match); cout << endl << “Volume of box2 = “ << box2.Volume(); cout << endl << “Surface area of box2 = “ << BoxSurface(box2); cout << endl; return 0; }
How It Works You declare the function BoxSurface() as a friend of the CBox class by writing the function prototype with the keyword friend at the front. Since the BoxSurface() function itself is a global function, it makes no difference where you put the friend declaration within the definition of the class, but it’s a good idea to be consistent when you position this sort of declaration. You can see that I have chosen to position ours after all the public and private members of the class. Remember that a friend function isn’t a member of the class, so access attributes don’t apply. The definition of the function follows that of the class. Note that you specify access to the data members of the object within the definition of BoxSurface(), using the CBox object passed to the function as a parameter. Because a friend function isn’t a class member, the data members can’t be referenced just by their names. They each have to be qualified by the object name in exactly the same way as they might in an ordinary function, except, of course, that an ordinary function can’t access the private members of a class. A friend function is the same as an ordinary function, except that it can access all the members of the class or classes for which it is a friend without restriction.
355
Chapter 7 The example produces the following output: Constructor called. Constructor called. Volume of match = 1.21 Surface area of match = 8.14 Volume of box2 = 1 Surface area of box2 = 6
This is exactly what you would expect. The friend function is computing the surface area of the CBox objects from the values of the private members.
Placing friend Function Definitions Inside the Class You could have combined the definition of the function with its declaration as a friend of the CBox class within the class definition and the code would run as before. The function definition in the class would be: friend double BoxSurface(CBox aBox) { return 2.0*(aBox.m_Length*aBox.m_Width + aBox.m_Length*aBox.m_Height + aBox.m_Height*aBox.m_Width); }
However, this has a number of disadvantages relating to the readability of the code. Although the function would still have global scope, this might not be obvious to readers of the code, because the function would be hidden in the body of the class definition.
The Default Copy Constructor Suppose that you declare and initialize a CBox object box1 with this statement: CBox box1(78.0, 24.0, 18.0);
You now want to create another CBox object, identical to the first. You would like to initialize the second CBox object with box1. Let’s try it.
Try It Out
Copying Information Between Instances
The following example shows this in action: // Ex7_09.cpp // Initializing an object with an object of the same class #include using std::cout; using std::endl; class CBox { public: // Constructor definition
356
// Class definition at global scope
Defining Your Own Data Types CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main() { CBox box1(78.0, 24.0, 18.0); CBox box2 = box1; cout << << << <<
// Initialize box2 with box1
endl “box1 volume = “ << box1.Volume() endl “box2 volume = “ << box2.Volume();
cout << endl; return 0; }
This example produces the following output: Constructor called. box1 volume = 33696 box2 volume = 33696
How It Works Clearly, the program is working as you would want, with both boxes having the same volume. However, as you can see from the output, our constructor was called only once for the creation of box1. The question is, how was box2 created? The mechanism is similar to the one that you experienced when you had no constructor defined and the compiler supplied a default constructor to allow an object to be created. In this case, the compiler generates a default version of what is referred to as a copy constructor. A copy constructor does exactly what we’re doing here — it creates an object of a class by initializing it with an existing object of the same class. The default version of the copy constructor creates the new object by copying the existing object, member by member.
357
Chapter 7 This is fine for simple classes such as CBox, but for many classes — classes that have pointers or arrays as members for example — it won’t work properly. Indeed, with such classes the default copy constructor can create serious errors in your program. In these cases, you must create your own class copy constructor. This requires a special approach that you’ll look into more fully towards the end of this chapter and again in the next chapter.
The Pointer this In the CBox class, you wrote the Volume() function in terms of the class member names in the definition of the class. Of course, every object of type CBox that you create contains these members so there has to be a mechanism for the function to refer to the members of the particular object for which the function is called. When any member function executes, it automatically contains a hidden pointer with the name this, which points to the object used with the function call. Therefore, when the member m_Length is accessed in the Volume() function during execution, it’s actually referring to this->m_Length, which is the fully-specified reference to the object member that is being used. The compiler takes care of adding the necessary pointer name this to the member names in the function. If you need to, you can use the pointer this explicitly within a member function. You might, for example, want to return a pointer to the current object.
Try It Out
Explicit Use of this
You could add a public function to the CBox class that compares the volume of two CBox objects. // Ex7_10.cpp // Using the pointer this #include using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height;
358
Defining Your Own Data Types } // Function to compare two boxes which returns true (1) // if the first is greater than the second, and false (0) otherwise int Compare(CBox xBox) { return this->Volume() > xBox.Volume(); } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main() { CBox match(2.2, 1.1, 0.5); CBox cigar(8.0, 5.0,1.0);
// Declare match box // Declare cigar box
if(cigar.Compare(match)) cout << endl << “match is smaller than cigar”; else cout << endl << “match is equal to or larger than cigar”; cout << endl; return 0; }
How It Works The member function Compare() returns true if the prefixed CBox object in the function call has a greater volume than the CBox object specified as an argument, and false if it doesn’t. In the return statements, the prefixed object is referred to through the pointer this, used with the indirect member access operator, ->, that you saw earlier in this chapter. Remember that you use the direct member access operator when accessing members through objects and the indirect member access operator when accessing members through pointers to objects. this is a pointer so you use the -> operator. The -> operator works the same for pointers to class objects as it did when you were dealing with a struct. Here, using the pointer this demonstrates that it exists and does work, but it’s quite unnecessary to use it explicitly in this case. If you change the return statement in the Compare() function to be return Volume() > xBox.Volume();
you’ll find that the program works just as well. Any references to unadorned member names are automatically assumed to be the members of the object pointed to by this.
359
Chapter 7 You use the Compare() function in main() to check the relationship between the volumes of the objects match and cigar. The output from the program is: Constructor called. Constructor called. match is smaller than cigar
This confirms that the cigar object is larger than the match object. It also wasn’t essential to define the Compare() function as a class member. You could just as well have written it as an ordinary function with the objects as arguments. Note that this isn’t true of the function Volume(), because it needs to access the private data members of the class. Of course, if you implemented the Compare()function as an ordinary function, it wouldn’t have access to the pointer this, but it would still be very simple: // Comparing two CBox objects - ordinary function version int Compare(CBox B1, CBox B2) { return B1.Volume() > B2.Volume(); }
This has both objects as arguments and returns true if the volume of the first is greater than the last. You would use this function to perform the same function as in the last example with this statement: if(Compare(cigar, match)) cout << endl << “match is smaller than cigar”; else cout << endl << “match is equal to or larger than cigar”;
If anything, this looks slightly better and easier to read than the original version; however, there’s a much better way to do this, which you will learn about in the next chapter.
const Objects of a Class The Volume() function that you defined for the CBox class does not alter the object for which it is called, neither does a function such as getHeight() that returns the value of the m_Height member. Likewise, the Compare() function in the previous example didn’t change the class objects at all. This may seem at first sight to be a mildly interesting but largely irrelevant observation, but it isn’t, it’s quite important. Let’s think about it. You will undoubtedly want to create class objects that are fixed from time to time, just like values such as pi or inchesPerFoot that you might declare as const double. Suppose you wanted to define a CBox object as const(because it was a very important standard sized box, for instance. You might define it with the following statement: const CBox standard(3.0, 5.0, 8.0);
360
Defining Your Own Data Types Now that you have defined your standard box having dimensions 3x5x8, you don’t want it messed about with. In particular, you don’t want to allow the values stored in its data members to be altered. How can you be sure they won’t be? Well, you already are. If you declare an object of a class as const, the compiler will not allow any member function to be called for it that might alter it. You can demonstrate this quite easily by modifying the declaration for the object, cigar, in the previous example to: const CBox cigar(8.0, 5.0,1.0);
// Declare cigar box
If you try recompiling the program with this change, it won’t compile. You see the error message: error C2662: ‘compare’ : cannot convert ‘this’ pointer from ‘const class CBox’ to ‘class CBox &’ Conversion loses qualifiers
This is produced for the if statement that calls the Compare() member of cigar. An object that you declare as const will always have a this pointer that is const, so the compiler will not allow any member function to be called that does not assume the this pointer that is passed to it is const. You need to find out how to make the this pointer in a member function const.
const Member Functions of a Class To make the this pointer in a member function const, you must declare the function as const within the class definition. Take a look at how you do that with the Compare() member of CBox. The class definition needs to be modified to the following: class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() { return m_Length*m_Width*m_Height; } // Function to compare two boxes which returns true (1) // if the first is greater than the second, and false (0) otherwise int Compare(CBox xBox) const { return this->Volume() > xBox.Volume(); } private:
361
Chapter 7 double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
};
To specify that a member function is const, you just append the const keyword to the function header. Note that you can only do this with class member functions, not with ordinary global functions. Declaring a function as const is only meaningful in the case of a function that is a member of a class. The effect is to make the this pointer in the function const, which in turn means that you cannot write a data member of the class on the left of an assignment within the function definition; it will be flagged as an error by the compiler. A const member function cannot call a non-const member function of the same class, since this would potentially modify the object. When you declare an object as const, the member functions that you call for it must be declared as const; otherwise the program will not compile.
Member Function Definitions Outside the Class When the definition of a const member function appears outside the class, the header for the definition must have the keyword const added, just as the declaration within the class does. In fact, you should always declare all member functions that do not alter the class object for which they are called as const. With this in mind, the CBox class could be defined as: class CBox // Class definition at global scope { public: // Constructor CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0); double Volume() const; int Compare(CBox xBox) const; private: double m_Length; double m_Width; double m_Height;
// Calculate the volume of a box // Compare two boxes
// Length of a box in inches // Width of a box in inches // Height of a box in inches
};
This assumes that all function members are defined separately, including the constructor. Both the Volume() and Compare() members have been declared as const. The Volume() function is now defined outside the class as: double CBox::Volume() const { return m_Length*m_Width*m_Height; }
The Compare() function definition is: int CBox::Compare(CBox xBox) const { return this->Volume() > xBox.Volume(); }
362
Defining Your Own Data Types As you can see, the const modifier appears in both definitions. If you leave it out, the code will not compile. A function with a const modifier is a different function from one without, even though the name and parameters are exactly the same. Indeed you can have both const and non-const versions of a function in a class, and sometimes this can be very useful. With the class declared as shown, the constructor also needs to be defined separately, like this: CBox::CBox(double lv, double bv, double hv): m_Length(lv), m_Width(bv), m_Height(hv) { cout << endl << “Constructor called.”; }
Arrays of Objects of a Class You can declare an array of objects of a class in exactly the same way that you have declared an ordinary array where the elements were one of the built-in types. Each element of an array of class objects causes the default constructor to be called.
Try It Out
Arrays of Class Objects
We can use the class definition of CBox from the last example but modified to include a specific default constructor: // Ex7_11.cpp // Using an array of class objects #include using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } CBox() // Default constructor { cout << endl << “Default constructor called.”; m_Length = m_Width = m_Height = 1.0; } // Function to calculate the volume of a box double Volume() const {
363
Chapter 7 return m_Length*m_Width*m_Height; } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main() { CBox boxes[5]; CBox cigar(8.0, 5.0, 1.0); cout << << << <<
// Array of CBox objects declared // Declare cigar box
endl “Volume of boxes[3] = “ << boxes[3].Volume() endl “Volume of cigar = “ << cigar.Volume();
cout << endl; return 0; }
The program produces this output: Default constructor called. Default constructor called. Default constructor called. Default constructor called. Default constructor called. Constructor called. Volume of boxes[3] = 1 Volume of cigar = 40
How It Works You have modified the constructor accepting arguments so that only two default values are supplied, and you have added a default constructor that initializes the data members to 1 after displaying a message that it was called. You are now able to see which constructor was called when. The constructors now have quite distinct parameter lists, so there’s no possibility of the compiler confusing them. You can see from the output that the default constructor was called five times, once for each element of the boxes array. The other constructor was called to create the cigar object. It’s clear from the output that the default constructor initialization is working satisfactorily, as the volume of the array element is 1.
Static Members of a Class Both data members and function members of a class can be declared as static. Because the context is a class definition, there’s a little more to it than the effect of the static keyword outside of a class, so let’s look at static data members.
364
Defining Your Own Data Types
Static Data Members of a Class When you declare data members of a class to be static, the effect is that the static data members are defined only once and are shared between all objects of the class. Each object gets its own copies of each of the ordinary data members of a class, but only one instance of each static data member exists, regardless of how many class objects have been defined. Figure 7-7 illustrates this. Class Definition class CBox { public: static int objectCount; ... private: double m_Length; double m_Width; double m_Height; ...
object1
object2
m_Length m_Width m_Height ...
m_Length m_Width m_Height ...
object3 m_Length m_Width m_Height ...
One copy of each static data member is shared between all objects of the class type
objectCount
Figure 7-7
One use for a static data member is to count how many objects actually exist. You could add a static data member to the public section of the CBox class by adding the following statement to the previous class definition: static int objectCount;
// Count of objects in existence
You now have a problem. How do you initialize the static data member? You can’t initialize the static data member in the class definition — that’s simply a blueprint for an object, and initializing values are not allowed. You don’t want to initialize it in a constructor because you want to increment it every time the constructor is called so the count of the number of objects created is accumulated. You can’t initialize it in another member function because a member function is associated with
365
Chapter 7 an object, and you want it initialized before any object is created. The answer is to write the initialization of the static data member outside of the class definition with this statement: int CBox::objectCount = 0;
// Initialize static member of class CBox
Notice that the static keyword is not included here; however, you do need to qualify the member name by using the class name and the scope resolution operator so that the compiler understands that you are referring to a static member of the class. Otherwise, you would simply create a global variable that was nothing to do with the class.
Try It Out
Counting Instances
Let’s add the static data member and the object counting capability to the last example. // Ex7_12.cpp // Using a static data member in a class #include using std::cout; using std::endl; class CBox { public: static int objectCount;
// Class definition at global scope
// Count of objects in existence
// Constructor definition CBox(double lv, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; objectCount++; } CBox() // Default constructor { cout << endl << “Default constructor called.”; m_Length = m_Width = m_Height = 1.0; objectCount++; } // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } private: double m_Length; double m_Width; double m_Height;
366
// Length of a box in inches // Width of a box in inches // Height of a box in inches
Defining Your Own Data Types }; int CBox::objectCount = 0;
// Initialize static member of CBox class
int main() { CBox boxes[5]; CBox cigar(8.0, 5.0, 1.0);
// Array of CBox objects declared // Declare cigar box
cout << endl << endl << “Number of objects (through class) = “ << CBox::objectCount; cout << endl << “Number of objects (through object) = “ << boxes[2].objectCount; cout << endl; return 0; }
This example produces the following output: Default constructor Default constructor Default constructor Default constructor Default constructor Constructor called.
called. called. called. called. called.
Number of objects (through class) = 6 Number of objects (through object) = 6
How It Works This code shows that it doesn’t matter how you refer to the static member ObjectCount (whether through the class itself or any of the objects of that class). The value is the same and it is equal to the number of objects of that class that have been created. The six objects are obviously the five elements of the Boxes array, plus the cigar object. It’s interesting to note that static members of a class exist even though there may be no members of the class in existence. This is evidently the case, because you initialized the static member ObjectCount before any class objects were declared. Static data members are automatically created when your program begins, and they will be initialized with 0 unless you initialize them with some other value. Thus you need only to initialize static data members of a class if you want them to start out with a value other than 0.
Static Function Members of a Class By declaring a function member as static, you make it independent of any particular object of the class. Referencing members of the class from within a static function must be done using qualified names (as you would do with an ordinary global function accessing a public data member). The static member function has the advantage that it exists, and can be called, even if no objects of the class exist.
367
Chapter 7 In this case, only static data members can be used because they are the only ones that exist. Thus, you can call a static function member of a class to examine static data members, even when you do not know for certain that any objects of the class exist. You could, therefore, use a static member function to determine whether some objects of the class have been created or, indeed, how many have been created. Of course, after the objects have been defined, a static member function can access private as well as public members of class objects. A static function might have this prototype: static void Afunction(int n);
A static function can be called in relation to a particular object by a statement such as the following: aBox.Afunction(10);
where aBox is an object of the class. The same function could also be called without reference to an object. In this case, the statement would take the following form, CBox::Afunction(10);
where CBox is the class name. Using the class name and the scope resolution operator serves to tell the compiler to which class the function Afunction() belongs.
Pointers and References to Class Objects Using pointers, and particularly references to class objects, is very important in object-oriented programming and in particular in the specification of function parameters. Class objects can involve considerable amounts of data, so using the pass-by-value mechanism by specifying parameters to a function to be objects can be very time consuming and inefficient because each argument object will be copied. There are also some techniques involving the use of references that are essential to some operations with classes. As you’ll see, you can’t write a copy constructor without using a reference parameter.
Pointers to Class Objects You declare a pointer to a class object in the same way that you declare other pointers. For example, a pointer to objects of the class CBox is declared in this statement: CBox* pBox = 0;
// Declare a pointer to CBox
You can now use this to store the address of a CBox object in an assignment in the usual way, using the address operator: pBox = &cigar;
// Store address of CBox object cigar in pBox
As you saw when you used the this pointer in the definition of the Compare() member function, you can call a function using a pointer to an object. You can call the function Volume() for the pointer pBox in a statement like this: cout << pBox->Volume();
368
// Display volume of object pointed to by pBox
Defining Your Own Data Types Again, this uses the indirect member selection operator. This is the typical notation used by most programmers for this kind of operation, so from now on I’ll use it universally.
Try It Out
Pointers to Classes
Let’s try exercising the indirect member access operator a little more. We will use the example Ex7_10.cpp as a base, but change it a little. // Ex7_13.cpp // Exercising the indirect member access operator #include using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0) { cout << endl << “Constructor called.”; m_Length = lv; // Set values of m_Width = bv; // data members m_Height = hv; } // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } // Function to compare two boxes which returns true (1) // if the first is greater that the second, and false (0) otherwise int Compare(CBox* pBox) const { return this->Volume() > pBox->Volume(); } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main() { CBox boxes[5]; CBox match(2.2, 1.1, 0.5); CBox cigar(8.0, 5.0, 1.0);
// Array of CBox objects declared // Declare match box // Declare cigar Box
369
Chapter 7 CBox* pB1 = &cigar; CBox* pB2 = 0; cout << << << << <<
// Initialize pointer to cigar object address // Pointer to CBox initialized to null
endl “Address of cigar is “ << pB1 endl “Volume of cigar is “ pB1->Volume();
// Display address
// Volume of object pointed to
pB2 = &match; if(pB2->Compare(pB1)) // Compare via pointers cout << endl << “match is greater than cigar”; else cout << endl << “match is less than or equal to cigar”; pB1 = boxes; // Set to address of array boxes[2] = match; // Set 3rd element to match cout << endl // Now access thru pointer << “Volume of boxes[2] is “ << (pB1 + 2)->Volume(); cout << endl; return 0; }
If you run the example, the output looks something like that shown here: Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Address of cigar is 0012FE20 Volume of cigar is 40 match is less than or equal to cigar Volume of boxes[2] is 1.21
Of course, the value of the address for the object cigar may well be different on your PC.
How It Works The only change to the class definition isn’t one of great substance. You have only modified the Compare() function to accept a pointer to a CBox object as an argument, and now we know about const member functions we declare it as const because it doesn’t alter the object. The function main() merely exercises pointers to CBox type objects in various, rather arbitrary, ways. Within the main()function you declare two pointers to CBox objects after declaring an array, Boxes, and the CBox objects cigar and match. The first, pB1, is initialized with the address of the object cigar, and the second, pB2, is initialized to NULL. All of this uses the pointer in exactly the same way you would when you’re applying a pointer to a basic type. The fact that you are using a pointer to a type that you have defined yourself makes no difference.
370
Defining Your Own Data Types You use pB1 with the indirect member access operator to generate the volume of the object pointed to, and the result is displayed. You then assign the address of match to pB2 and use both pointers in calling the compare function. Because the argument of the function Compare() is a pointer to a CBox object , the function uses the indirect member selection operator in calling the Volume() function for the object. To demonstrate that you can use address arithmetic on the pointer pB1 when using it to select the member function, you set pB1 to the address of the first element of the array of type CBox, boxes. In this case, you select the third element of the array and calculate its volume. This is the same as the volume of match. You can see from the output that there were seven calls of the constructor for CBox objects: five due to the array Boxes, plus one each for the objects cigar and match. Overall, there’s virtually no difference between using a pointer to a class object and using a pointer to a basic type, such as double.
References to Class Objects References really come into their own when they are used with classes. As with pointers, there is virtually no difference between the way you declare and use references to class objects and the way in which we’ve already declared and used references to variables of basic types. To declare a reference to the object cigar, for instance, you would write this: CBox& rcigar = cigar;
// Define reference to object cigar
To use a reference to calculate the volume of the object cigar, you would just use the reference name where the object name would otherwise appear: cout << rcigar.Volume();
// Output volume of cigar thru a reference
As you may remember, a reference acts as an alias for the object it refers to, so the usage is exactly the same as using the original object name.
Implementing a Copy Constructor The importance of references is really in the context of arguments and return values in functions, particularly class member functions. Let’s return to the question of the copy constructor as a first toe in the water. For the moment, I’ll sidestep the question of when you need to write your own copy constructor and concentrate on the problem of how you can write one. I’ll use the CBox class just to make the discussion more concrete. The copy constructor is a constructor that creates an object by initializing it with an existing object of the same class. It therefore needs to accept an object of the class as an argument. You might consider writing the prototype like this: CBox(CBox initB);
Now consider what happens when this constructor is called. If you write this declaration: CBox myBox = cigar;
371
Chapter 7 this generates a call of the copy constructor as follows: CBox::CBox(cigar);
This seems to be no problem, until you realize that the argument is passed by value. So, before the object cigar can be passed, the compiler needs to arrange to make a copy of it. Therefore, it calls the copy constructor to make a copy of the argument for the call of the copy constructor. Unfortunately, since it is passed by value, this call also needs a copy of its argument to be made, so the copy constructor is called and so on and so on. You end up with an infinite number of calls to the copy constructor. The solution, as I’m sure you have guessed, is to use a const reference parameter. You can write the prototype of the copy constructor like this: CBox(const CBox& initB);
Now, the argument to the copy constructor doesn’t need to be copied. It is used to initialize the reference parameter, so no copying takes place. As you remember from the discussion on references, if a parameter to a function is a reference, no copying of the argument occurs when the function is called. The function accesses the argument variable in the caller function directly. The const qualifier ensures that the argument can’t be modified in the function. This is another important use of the const qualifier. You should always declare a reference parameter of a function as const unless the function will modify it. You could implement the copy constructor as follows: CBox::CBox(const CBox& initB) { m_Length = initB.m_Length; m_Width = initB.m_Width; m_Height = initB.m_Height; }
This definition of the copy constructor assumes that it appears outside of the class definition. The constructor name is therefore qualified with the class name using the scope resolution operator. Each data member of the object being created is initialized with the corresponding member of the object passed as an argument. Of course, you could equally well use the initialization list to set the values of the object. This case is not an example of when you need to write a copy constructor. As you have seen, the default copy constructor works perfectly well with CBox objects. I will get to why and when you need to write your own copy constructor in the next chapter.
C++/CLI Programming The C++/CLI programming language has its own struct and class types. In fact C++/CLI allows the definition of two different struct and class types that have different characteristics; value struct types and value class types, and ref struct types and ref class types. Each of the two word combinations, value struct, ref struct, value class, and ref class, is a keyword and distinct
372
Defining Your Own Data Types from the keywords struct and class; value and ref are not by themselves keywords. As in native C++, the only difference between a struct and a class in C++/CLI is that members of a struct are public by default whereas members of a class are private by default. One essential difference between value classes (or value structs) and reference classes (or ref structs) is that variables of value class types contain their own data whereas variables to access reference class types must be handles and therefore contain an address. Note that member functions of C++/CLI classes cannot be declared as const. Another difference from native C++ is that the this pointer in a non-static function member of a value class type T is an interior pointer of type interior_ptr, whereas the this pointer in a ref class type T is a handle of type T^. You need to keep this in mind when returning the this pointer from a C++/CLI function or storing it in a local variable. There are three other restrictions that apply to both value classes and reference classes: ❑
A value class or ref class cannot contain fields that are native C++ arrays or native C++ class types.
❑
Friend functions are not allowed.
❑
A value class or ref class cannot have members that are bit-fields.
You have already heard back in Chapter 4 that the fundamental type names such as type int and type double are shorthand for value class types in a CLR program. When you declare a data item of a value class type, memory for it will be allocated on the stack but you can create value class objects on the heap using the gcnew operator, in which case the variable that you use to access the value class object must be a handle. For example: double pi = 3.142; int^ lucky = gcnew int(7); double^ two = 2.0;
// pi is stored on the stack // lucky is a handle and 7 is stored on the heap // two is a handle, and 2.0 is stored on the heap
You can use any of these variables in arithmetic expression but you must use the * operator to dereference the handle to access the value. For example: Console::WriteLine(L”2pi = {0}”, *two*pi);
Note that you could write the product as pi**two and get the right result but it is better to use parentheses in such instances and write pi*(*two), as this makes the code clearer.
Defining Value Class Types I won’t discuss value struct types separately from value class types as the only difference is that the members of a value struct type are public by default whereas value class members are private by default. A value class is intended to be a relatively simple class type that provides the possibility for you to define new primitive types that can be used in a similar way to the fundamental types; however, you won’t be in a position to do this fully until you learn about a topic called operator overloading in the next chapter. A variable of a value class type is created on the stack and stores the value directly, but as you have already seen, you can also use a tracking handle to reference a value class type stored on the CLR heap.
373
Chapter 7 Take an example of a simple value class definition: // Class representing a height value class Height { private: // Records the height in feet and inches int feet; int inches; public: // Create a height from inches value Height(int ins) { feet = ins/12; inches = ins%12; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} };
This defines a value class type with the name Height. It has two private fields that are both of type int that record a height in feet and inches. The class has two constructors — one to create a Height object from a number of inches supplied as the argument, and the other to create a Height object from both a feet and inches specification. The latter should really check that the number of inches supplied as an argument is less than 12, but I’ll leave you to add that as a fine point. To create a variable of type Height you could write: Height tall = Height(7, 8);
// Height is 7 feet 8 inches
This creates the variable, tall, containing a Height object representing 7 feet 8 inches; this object is created by calling the constructor with two parameters. Height baseHeight;
This statement creates a variable, baseHeight, that will be automatically initialized to a height of zero. The Height class does not have a no-arg constructor specified and because it is a value class, you are not permitted to supply one in the class definition. There will be a no-arg constructor included automatically in a value class that will initialize all value type fields to the equivalent of zero and all fields that are handles to nullptr and you cannot override the implicit constructor with your own version. It’s this default constructor that will be used to create the value of baseHeight. There are a couple of other restrictions on what a value class can contain: ❑
You must not include a copy constructor in a value class definition.
❑
You cannot override the assignment operator in a value class (I’ll discuss how you override operators in a class in Chapter 8).
Value class objects are always copied by just copying fields and the assignment of one value class object to another is done in the same way. Value classes are intended to be used to represent simple objects
374
Defining Your Own Data Types defined by a limited amount of data so for objects that don’t fit this specification or where the value class restrictions are problematical you should use ref class types to represent them. Let’s take the Height class for a test drive.
Try It Out
Defining and Using a Value Class Type
Here’s the code to exercise the Height value class: // Ex7_14.cpp : main project file. // Defining and using a value class type #include “stdafx.h” using namespace System; // Class representing a height value class Height { private: // Records the height in feet and inches int feet; int inches; public: // Create a height from inches value Height(int ins) { feet = ins/12; inches = ins%12; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} }; int main(array<System::String ^> ^args) { Height myHeight = Height(6,3); Height^ yourHeight = Height(70); Height hisHeight = *yourHeight; Console::WriteLine(L”My height is {0}”, myHeight); Console::WriteLine(L”Your height is {0}”, yourHeight); Console::WriteLine(L”His height is {0}”, hisHeight); return 0; }
Executing this program results in the following output: My height is Height Your height is Height His height is Height Press any key to continue . . .
375
Chapter 7 How It Works Well, the output is a bit monotonous and perhaps less than we were hoping for, but let’s come back to that a little later. In the main() function, you create three variables with the following statement: Height myHeight = Height(6,3); Height^ yourHeight = Height(70); Height hisHeight = *yourHeight;
The first variable is of type Height so the object that represents a height of 6 feet 3 inches is allocated on the stack. The second variable is a handle of type Height^ so the object representing a height of 5 feet 10 inches is created on the CLR heap. The third variable is another stack variable that is a copy of the object referenced by yourHeight. Because yourHeight is a handle, you have to dereference it to assign it to the hisHeight variable and the result is that hisHeight contains a duplicate of the object referenced by yourHeight. Variables of a value class type always contain a unique object so two such variables cannot reference the same object; assigning one variable of a value class type to another always involves copying. Of course, several handles can reference a single object and assigning the value of one handle to another simply copies the address (or nullptr) from one to the other so that both objects reference the same object. The output is produced by the three calls of the Console::WriteLine() function. Unfortunately the output is not the values of the value class objects, but simply the class name. So how did this come about? It was optimistic to expect the values to be produced — after all, how is the compiler to know how they should be presented? Height objects contain two values — which one should be presented as the value? The class has to have a way to make the value available in this context.
The ToString() Function in a Class Every C++/CLI class that you define has a ToString() function — I’ll explain how this comes about in the next chapter when I discuss class inheritance — that returns a handle to a string that is supposed to represent the class object. The compiler arranges for the ToString() function for an object to be called whenever it recognizes that a string representation of an object is required and you can call it explicitly if necessary. For example, you could write this: double pi = 3.142; Console::WriteLine(pi.ToString());
This outputs the value of pi as a string and it is the ToString() function that is defined in the System::Double class that provides the string. Of course, you would get the same output without explicitly calling the ToString() function. The default version of the ToString() function that you get in the Height class just outputs the class name because there is no way to know ahead of time what value should be returned as a string for an object of your class type. To get an appropriate value output by the Console::WriteLine() function in the previous example, you must add a ToString() function to the Height class that presents the value of an object in the form that you want. Here’s how the class looks with a ToString() function: // Class representing a height value class Height
376
Defining Your Own Data Types { private: // Records the height in feet and inches int feet; int inches; public: // Create a height from inches value Height(int ins) { feet = ins/12; inches = ins%12; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} // Create a string representation of the object virtual String^ ToString() override { return feet + L” feet “+ inches + L” inches”; } };
The combination of the virtual keyword before the return type for ToString() and the override keyword following the parameter list for the function indicates that this version of the ToString() function overrides the version of the function that is present in the class by default. You’ll hear a lot more about this in Chapter 8. Our new version of the ToString() function now output a string expressing a height in feet and inches. If you add this function to the class definition in the previous example, you get the following output when you compile and execute the program: My height is 6 feet 3 inches Your height is 5 feet 10 inches His height is 5 feet 10 inches Press any key to continue . . .
This is more like what you were expecting to get before. You can see from the output that the WriteLine() function quite happily deals with an object on the CLR heap that you references through the yourHeight handle, as well as the myHeight and hisHeight objects that were created on the stack.
Literal Fields The factor 12 that you use to convert from feet to inches and vice versa is a little troubling. It is an example of what is called a “magic number,” where a person reading the code has to guess or deduce its significance and origin. In this case it’s fairly obvious what the 12 is but there will be many instances where the origin of a numerical constant in a calculation is not so apparent. C++/CLI has a literal field facility for introducing named constants into a class that will solve the problem in this case. Here’s how you can eliminate the magic number from the code in the single-argument constructor in the Height class: value class Height { private:
377
Chapter 7 // Records the height in feet and inches int feet; int inches; literal int inchesPerFoot = 12; public: // Create a height from inches value Height(int ins) { feet = ins/ inchesPerFoot; inches = ins% inchesPerFoot; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} // Create a string representation of the object virtual String^ ToString() override { return feet + L” feet “+ inches + L” inches”; } };
Now the constructor uses the name inchesPerFoot instead of 12, so there is no doubt as to what is going on. You can define the value of a literal field in terms of other literal fields as long as the names of the fields you are using to specify the value are defined first. For example: value class Height { // Other code... literal int inchesPerFoot = 12; literal double millimetersPerInch = 25.4; literal double millimetersPerFoot = inchesPerFoot*millimetersPerInch; // Other code... }
Here you define the value for the literal field millimetersPerFoot as the product of the other two literal fields. If you were to move the definition of the millimetersPerFoot field so that it precedes either or both of the other two, the code would not compile.
Defining Reference Class Types A reference class is comparable to a native C++ class in capabilities and does not have the restrictions that a value class has. Unlike a native C++ class, however, a reference class does not have a default copy constructor or a default assignment operator. If your class needs to support either of these operators, you must explicitly add a function for the capability — you’ll see how in the next chapter.
378
Defining Your Own Data Types You define a reference class using the ref class keyword — both words together separated by one or more spaces represent a single keyword. Here’s the CBox class from the Ex7_07 example redefined as a reference class. ref class Box { public: // No-arg constructor supplying default field values Box(): Length(1.0), Width(1.0), Height(1.0) { Console::WriteLine(L”No-arg constructor called.”); } // Constructor definition using an initialisation list Box(double lv, double bv, double hv): Length(lv), Width(bv), Height(hv) { Console::WriteLine(L”Constructor called.”); } // Function to calculate the volume of a box double Volume() { return Length*Width*Height; } private: double Length; double Width; double Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
};
Note first that I have removed the C prefix for the class name and the m_ prefix for member names because this notation is not recommended for C++/CLI classes. You cannot specify default values for function and constructor parameters in C++/CLI classes so you have to add a no-arg constructor to the Box class to fulfill this function. The no-arg constructor just initializes the three private fields with 1.0.
Try It Out
Using a Reference Class Type
Here’s an example that uses the Box class that you saw in the previous section. // Ex7_15.cpp : main project file. // Using the Box reference class type #include “stdafx.h” using namespace System; ref class Box { public: // No-arg constructor supplying default field values Box(): Length(1.0), Width(1.0), Height(1.0) {
379
Chapter 7 Console::WriteLine(L”No-arg constructor called.”); } // Constructor definition using an initialisation list Box(double lv, double bv, double hv): Length(lv), Width(bv), Height(hv) { Console::WriteLine(L”Constructor called.”); } // Function to calculate the volume of a box double Volume() { return Length*Width*Height; } private: double Length; double Width; double Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int main(array<System::String ^> ^args) { Box^ aBox; // Handle of type Box^ Box^ newBox = gcnew Box(10, 15, 20); aBox = gcnew Box; // Initialize with default Box Console::WriteLine(L”Default box volume is {0}”, aBox->Volume()); Console::WriteLine(L”New box volume is {0}”, newBox->Volume()); return 0; }
The output from this example is: Constructor called. No-arg constructor called. Default box volume is 1 New box volume is 3000 Press any key to continue . . .
How It Works The first statement in main() creates a handle to a Box object. Box^ aBox;
// Handle of type Box^
No object is created by this statement, just the tracking handle, aBox. The aBox variable is initialized by default with nullptr so it does not point to anything yet. In contrast, a variable of a value class type always contains an object. The next statement creates a handle to a new Box object. Box^ newBox = gcnew Box(10, 15, 20);
380
Defining Your Own Data Types The constructor accepting three arguments is called to create the Box object on the heap and the address is stored in the handle, newBox. As you know, objects of ref class types are always created on the CLR heap and are always referenced using a handle. You create a Box object by calling the no-arg constructor and store its address in aBox. aBox = gcnew Box;
// Initialize with default Box
This object has the Length, Width, and Height fields set to 1.0. Finally you output the volumes of the two Box objects you have created. Console::WriteLine(L”Default box volume is {0}”, aBox->Volume()); Console::WriteLine(L”New box volume is {0}”, newBox->Volume());
Because aBox and newBox are handles, you use the -> operator to call the Volume() function for the objects to which they refer.
Class Properties A property is a member of either a value class or a reference class that you access as though it were a field, but it really isn’t a field. The primary difference between a property and a field is that the name of a field refers to a storage location whereas the name of a property does not — it calls a function. A property has get() and set() accessor functions to retrieve and set its value respectively so when you use a property name to obtain its value, behind the scenes you are calling the get() function for the property and when you use the name of a property on the right of an assignment statement you are calling its set() function. If a property only provides a definition for the get() function, it is called a read-only property because the set() function is not available to set the property value. A property may just have the set() function defined for it, in which case it is described as a write-only property. A class can contain two different kinds of properties: scalar properties and indexed properties. Scalar properties are a single value accessed using the property name whereas indexed properties are a set of values that you access using an index between square brackets following the property name. The String class has the scalar property, Length, that provides you with the number of characters in a string, and for a String object str you access the Length property using the expression str->Length because str is a handle. Of course, to access a property with the name MyProp for a value class object store in the variable val, you would use the expression val.MyProp, just like accessing a field. The Length property for a string is an example of a read-only property because no set() function is defined for it — you cannot set the length of a string as a String object is immutable. The String class also gives you access to individual characters in the string as indexed properties. For a string handle, str, you can access the third indexed property with the expression str[2], which corresponds to the third character in the string. Properties can be associated with, and specific to, a particular object, in which case the properties are described as instance properties. The Length property for a String object is an example of an instance property. You can also specify a property as static, in which case the property is associated with the class and the property value will be the same for all objects of the class type. Let’s explore properties in a little more depth.
381
Chapter 7 Defining Scalar Properties As scalar property has a single value and you define a scalar property in a class using the property keyword. The get() function for a scalar property must have a return type that is the same as the property type and the set() function must have a parameter of the same type as the property. Here’s an example of a property in the Height value class that you saw earlier. value class Height { private: // Records the height in feet and inches int feet; int inches; literal int inchesPerFoot = 12; literal double inchesToMeters = 2.54/100; public: // Create a height from inches value Height(int ins) { feet = ins / inchesPerFoot; inches = ins % inchesPerFoot; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} // The height in meters as a property property double meters { // Returns the property value double get() { return inchesToMeters*(feet*inchesPerFoot+inches); } // You would define the set() function for the property here... } // Create a string representation of the object virtual String^ ToString() override { return feet + L” feet “+ inches + L” inches”; } };
The Height class now contains a property with the name meters. The definition of the get function for the property appears between the braces following the property name. You would put the set() function for the property here too, if there was one. Note that there is no semicolon following the braces that enclose the get() and set() function definitions for a property. The get() function for the meters property makes use of a new literal class member, inchesToMeters, to convert the height in inches to meters. Accessing the meters property for an object of type Height makes the height available in meters. Here’s how you could do that:
382
Defining Your Own Data Types Height ht = Height(6, 8); // Height of 6 feet 8 inches Console::WriteLine(L”The height is {0} meters”, ht->meters);
The second statement outputs the value of ht in meters using the expression ht->meters. You are not obliged to define the get() and set() functions for a property inline; you can define them outside the class definition in a .cpp file. For example, you could specify the meters property in the Height class like this: value class Height { // Code as before... public: // Code as before... // The height in meters property double meters { double get();
// Returns the property value
// You would define the set() function for the property here... } // Code as before... };
The get() function for the meters property is now declared but not defined in the Height class, so a definition must be supplied outside the class definition. In the get() function definition in the Height.cpp file the function name must be qualified by the class name and the property name so the definition looks like this: Height::meters::get() { return inchesToMeters*(feet*inchesPerFoot+inches); }
The Height qualifier indicates that this function definition belongs to the Height class and the meters qualifier indicates the function is for the meters property in the class. Of course, you can define properties for a reference class. Here’s an example: ref class Weight { private: int lbs; int oz; public: property int pounds { int get() { return lbs; } void set(int value) { lbs = value;
}
383
Chapter 7 } property int ounces { int get() { return oz; } void set(int value) { oz = value; }
}
};
Here the pounds and ounces properties are used to provide access to the values of the private fields, lbs and oz. You can set values the properties of a Weight object and access them subsequently like this: Weight^ wt = gcnew Weight; wt->pounds = 162; wt->ounces = 12; Console::WriteLine(L”Weight is {0} lbs {1} oz.”, wt->pounds, wt->ounces);
A variable accessing a ref class object is always a handle so you must use the -> operator to access properties of an object of a reference class type.
Trivial Scalar Properties You can define a scalar property for a class without providing definitions for the get() and set() functions, in which case it is called a trivial scalar property. To specify a trivial scalar property you just omit the braces containing the get() and set() function definitions and end the property declaration with a semi-colon. Here’s an example of a value class with trivial scalar properties: value class Point { public: property int x; property int y; virtual String^ ToString() override { return L”(“ + x + L”,” + y + L”)”; }
// Trivial property // Trivial property
// Returns “(x,y)”
};
Default get() and set() function definitions are supplied automatically for each trivial scalar property and these return the property value and set the property value to the argument of the type specified for the property. Private space is allocated to accommodate the property value behind the scenes. Let’s see some scalar properties working.
Try It Out
Using Scalar Properties
This example uses three classes, two value classes, and a ref class: // Ex7_16.cpp : main project file. // Using scalar properties
384
Defining Your Own Data Types #include “stdafx.h” using namespace System; // Class defining a person’s height value class Height { private: // Records the height in feet and inches int feet; int inches; literal int inchesPerFoot = 12; literal double inchesToMeters = 2.54/100; public: // Create a height from inches value Height(int ins) { feet = ins / inchesPerFoot; inches = ins % inchesPerFoot; } // Create a height from feet and inches Height(int ft, int ins) : feet(ft), inches(ins){} // The height in meters property double meters // Scalar property { // Returns the property value double get() { return inchesToMeters*(feet*inchesPerFoot+inches); } // You would define the set() function for the property here... } // Create a string representation of the object virtual String^ ToString() override { return feet + L” feet “+ inches + L” inches”; } }; // Class defining a person’s weight value class Weight { private: int lbs; int oz; literal int ouncesPerPound = 16;
385
Chapter 7 literal double lbsToKg = 1.0/2.2; public: Weight(int pounds, int ounces) { lbs = pounds; oz = ounces; } property int pounds { int get() { return lbs; } void set(int value) { lbs = value; } property int ounces { int get() { return oz; } void set(int value) { oz = value; }
// Scalar property
}
// Scalar property
}
property double kilograms // Scalar property { double get() { return lbsToKg*(lbs + oz/ouncesPerPound); } } virtual String^ ToString() override { return lbs + L” pounds “ + oz + L” ounces”; } }; // Class defining a person ref class Person { private: Height ht; Weight wt; public: property String^ Name;
// Trivial scalar property
Person(String^ name, Height h, Weight w) : ht(h), wt(w) { Name = name; } Height getHeight(){ return ht; Weight getWeight(){ return wt;
} }
}; int main(array<System::String ^> ^args) { Weight hisWeight = Weight(185, 7); Height hisHeight = Height(6, 3);
386
Defining Your Own Data Types Person^ him = gcnew Person(L”Fred”, hisHeight, hisWeight); Weight herWeight = Weight(105, 3); Height herHeight = Height(5, 2); Person^ her = gcnew Person(L”Freda”, herHeight, herWeight); Console::WriteLine(L”She is {0}”, her->Name); Console::WriteLine(L”Her weight is {0:F2} kilograms.”, her->getWeight().kilograms); Console::WriteLine(L”Her height is {0} which is {1:F2} meters.”, her->getHeight(),her->getHeight().meters); Console::WriteLine(L”He is {0}”, him->Name); Console::WriteLine(L”His weight is {0}.”, him->getWeight()); Console::WriteLine(L”His height is {0} which is {1:F2} meters.”, him->getHeight(),him->getHeight().meters); return 0; }
This example produces the following output: She is Freda Her weight is Her height is He is Fred His weight is His height is Press any key
47.73 kilograms. 5 feet 2 inches which is 1.57 meters. 185 pounds 7 ounces. 6 feet 3 inches which is 1.91 meters. to continue . . .
How It Works The two value classes, Height and Weight, define the height and weight of a person. The Person class has fields of type Height and Weight to store the height and weight of a person and the name of a person is stored in the Name property, which is a trivial property because no explicit get() and set() functions have been defined for it; it therefore has the default get() and set() functions. The first two statements in main() define Height and Weight objects and these are then used to define him: Weight hisWeight = Weight(185, 7); Height hisHeight = Height(6, 3); Person^ him = gcnew Person(L”Fred”, hisHeight, hisWeight); Height and Weight are value classes so the variables of these types store the values directly. Person is a reference class so him is a handle. The first argument to the Person class constructor is a string literal so the compiler arranges for a String object to be created from this and the handle to this object is passed as the argument. The second and third arguments are the value class objects that you create in the first two statements. Of course, copies of these are passed as arguments because of the pass-by-value mechanism for function arguments. Within the Person class constructor, the assignment statement sets the value of the Name parameter and the values of the two fields, ht and wt, are set in the initialization list. The only way to set a property is through an implicit call of its set() function; a property cannot be initialized in the initializer list of a constructor.
387
Chapter 7 There is a similar sequence of three statements to those for him that define her. With two Person objects created on the heap you first output information about her with these statements: Console::WriteLine(L”She is {0}”, her->Name); Console::WriteLine(L”Her weight is {0:F2} kilograms.”, her->getWeight().kilograms); Console::WriteLine(L”Her height is {0} which is {1:F2} meters.”, her->getHeight(),her->getHeight().meters);
In the first statement, you access the Name property for the object referenced by the handle, her, with the expression her->Name; the result of executing this is a handle to the string that the property’s get() value returns, so it is of type String^. In the second statement, you access the kilograms property of the wt field for the object referenced by her with the expression her->getWeight().kilograms. The her->getWeight() part of this expression returns a copy of the wt field, and this is used to access the kilograms property; thus the value returned by the get() function for the kilograms property becomes the value of the second argument to the WriteLine() function. In the third output statement, the second argument is the result of the expression, her->getHeight(), which returns a copy of ht. To produce the value of this in a form suitable for output the compiler arranges for the ToString() function for the object to be called, so the expression is equivalent to her->getHeight().ToString(), and in fact you could write it like this if you want. The third argument to the WriteLine() function is the meters property for the Height object that is returned by the getHeight() function the the Person object, her. The last three output statements output information about the him object in a similar fashion to the her object. In this case the weight is produced by an implicit call of the ToString() function for the wt field in the him object.
Defining Indexed Properties Indexed properties are a set of property values in a class that you access using an index between square brackets, just like accessing an array element. You have already made use of indexed properties for strings because the String class makes characters from the string available as indexed properties. As you have seen, if str is the handle to a String object then the expression str[4] accesses the fifth indexed property value, which corresponds to the fifth character in the string. A property that you access by placing an index in square brackets following the name of the variable referencing the object, this is an example of a default indexed property. An indexed property that has a property name is described as a named indexed property. Here’s a class containing a default indexed property: ref class Name { private: array<String^>^ Names;
// Stores names as array elements
public: Name(...array<String^>^ names) Names(names) {} // Indexed property to return any name
388
Defining Your Own Data Types property String^ default[int] { // Retrieve indexed property value String^ get(int index) { if(index >= Names->Length) throw gcnew Exception(L”Index out of range”); return Names[index]; } } };
The idea of the Name class is to store a person’s name as an array of individual names. The constructor accepts an arbitrary number of arguments of type String^ that are then stored in the Names field so a Name object can accommodate any number of names. The indexed property here is a default indexed property because the name is specified by the default keyword. If you supplied a regular name in this position in the property specification it would be a named indexed property. The square brackets following the default keyword indicate that it is indeed an indexed property and the type they enclose — type int in this case — is the type of the index values that is to be used when retrieving values for the property. The type of the index does not have to be a numeric type and you can have more than one index parameter for accessing indexed property values. For an indexed property accessed by a single index, the get() function must have a parameter specifying the index that is of the same as the type that appears between the square brackets following the property name. The set() function for such an indexed property must have two parameters: the first parameter is the index, and the second is the new value to be set for the property corresponding to the first parameter. Let’s see how indexed properties behave in practice.
Try It Out
Using a Default Indexed Property
Here’s an example that makes use of a slightly extended version of the Name class: // Ex7_17.cpp : main project file. // Defining and using default indexed properties #include “stdafx.h” using namespace System; ref class Name { private: array<String^>^ Names; public: Name(...array<String^>^ names) : Names(names) {} // Scalar property specifying number of names property int NameCount
389
Chapter 7 { int get() {return Names->Length; } } // Indexed property to return names property String^ default[int] { String^ get(int index) { if(index >= Names->Length) throw gcnew Exception(L”Index out of range”); return Names[index]; } } }; int main(array<System::String ^> ^args) { Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”, L”Inigo”, L”Whelkwhistle”); // List the names for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]); return 0; }
This example produces the following output: Name 1 is Ebenezer Name 2 is Isaiah Name 3 is Ezra Name 4 is Inigo Name 5 is Whelkwhistle Press any key to continue . . .
How It Works The Name class in the example is basically the same as in the previous section but with a scalar property with the name NameCount added that returns the number of names in the Name object. In main() you first create a Name object with five names: Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”, L”Inigo”, L”Whelkwhistle”);
The parameter list for the constructor in the Name class starts with an ellipsis so it accepts any number of arguments. The arguments that are supplied when it is called will be stored in the elements of the names array, so initializing the Names field with names makes Names reference the names array. In the previous statement, you supply five arguments to the constructor, so the Names field in the object referenced by myName is an array with five elements.
390
Defining Your Own Data Types You access the properties for myName in a for loop to list the names the object contains: for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]);
You use the NameCount property value to control the for loop. Without this property you would not know how many names there are to be listed. Within the loop the last argument to the WriteLine() function accesses the ith indexed property. As you see, accessing the default indexed property just involves placing the index value in square brackets after the myName variable name. The output demonstrates that the indexed properties are working as expected. The indexed property is read-only because the Name class only includes a get() function for the property. To allow properties to be changed you could add a definition for the set() function for the default indexed property like this: ref class Name { // Code as before... // Indexed property to return names property String^ default[int] { String^ get(int index) { if(index >= Names->Length) throw gcnew Exception(L”Index out of range”); return Names[index]; } void set(int index, String^ name) { if(index >= Names->Length) throw gcnew Exception(L”Index out of range”); Names[index] = name; } } };
You can now use the ability to set indexed properties by adding a statement in main() to set the last indexed property value: Name^ myName = gcnew Name(L”Ebenezer”, L”Isaiah”, L”Ezra”, L”Inigo”, L”Whelkwhistle”); myName[myName->NameCount - 1] = L”Oberwurst”; // Change last indexed property // List the names for(int i = 0 ; i < myName->NameCount ; i++) Console::WriteLine(L”Name {0} is {1}”, i+1, myName[i]);
With this addition you’ll see from the output for the new version of the program that the last name has indeed been updated by the statement assigning a new value to the property at index position myName->NameCount-1.
391
Chapter 7 You could also try adding a named indexed property to the class: ref class Name { // Code as before... // Indexed property to return initials property wchar_t Initials[int] { wchar_t get(int index) { if(index >= Names->Length) throw gcnew Exception(L”Index out of range”); return Names[index][0]; } } };
The indexed property has the name Initials because its function is to return the initial of a name specified by an index. You define a named indexed property in essentially the same way as a default indexed property but with the property name replacing the default keyword. If you recompile the program and execute it once more, you see the following output. Name 1 is Ebenezer Name 2 is Isaiah Name 3 is Ezra Name 4 is Inigo Name 4 is Oberwurst The initials are: E.I.E.I.O. Press any key to continue . . .
The initials are produced by accessing the named indexed property in the for loop and the output shows that they work as expected.
More Complex Indexed Properties As mentioned, indexed properties can be defined so that more than one index is necessary to access a value and the indexes need not be numeric. Here’s an example of a class with such an indexed property: enum class Day{Monday, Tuesday, Wednesday,Thursday, Friday, Saturday, Sunday}; // Class defining a shop ref class Shop { public: property String^ Opening[Day, String^] { String^ get(Day day, String^ AmOrPm) { switch(day) { case Day::Saturday:
392
// Opening times
// Saturday opening:
Defining Your Own Data Types if(AmOrPm == L”am”) return L”9:00”; else return L”14:30”; break; case Day::Sunday: return L”closed”; break; default: if(AmOrPm == L”am”) return L”9:30”; else return L”14:00”; break;
//
morning is 9 am
//
afternoon is 2:30 pm
// Saturday opening: // closed all day
// Monday to Friday opening: // morning is 9:30 am //
afternoon is 2 pm
} } } };
The class representing a shop has an indexed property specifying opening times. The first index is an enumeration value of type Day that identifies the day of the week and the second index is a handle to a string that determines whether it is morning or afternoon. You could output the Opening property value for a Shop object like this: Shop^ shop = gcnew Shop; Console::WriteLine(shop->Opening[Day::Saturday, L”pm”]);
The first statement creates the Shop object and the second displays the opening time for the shop for Saturday afternoon. As you see, you just place the two index values for the property between square brackets and separate them by a comma. The output from the second statement is the string “14:30”. If you can dream up a reason why you need them, you could also define indexed properties with three or more indexes in a class.
Static Properties Static properties are similar to static class members in that they are defined for the class and are the same for all objects of the class type. You define a static property by adding the static keyword in the definition of the property. Here’s how you might define a static property in the Length class you saw earlier: value class Length { // Code as before... public: static property String^ Units { String^ get() { return L”feet and inches”; } };
}
393
Chapter 7 This is a simple property that makes available the units assumed by the class as a string. You access a static property by qualifying the property name with the name of the class, just as you would any other static member of the class: Console::WriteLine(L”Class units are {0}.”, Length::Units);
Static class properties exist whether or not any objects of the class type have been created. This differs from instance properties, which are specific to each object of the class type. Of course, if you have defined a class object, you can access a static property using the variable name. If you have created a Length object with the name len for example, you could output the value of the static Units property with the statement: Console::WriteLine(L”Class units are {0}.”, len.Units);
For accessing a static property in a reference class through a handle to an object of that type you would use the -> operator.
Reserved Property Names Although properties are different from fields, the values for properties still have to be stored somewhere and the storage locations need to be identified somehow. Internally properties have names created for the storage locations that are needed, and such names are reserved in a class that has properties so you must not use these names for other purposes. If you define a scalar or named indexed property with the name NAME in a class, the names get_NAME and set_NAME are reserved in the class so you must not use them for other purposes. Both names are reserved regardless of whether or not you define the get() and set() functions for the property. When you define a default indexed property in a class, the names get_Item and set_Item are reserved. The possibility of there being reserved names that use underscore characters is a good reason for avoiding the use of the underscore character in your own names in a C++/CLI program.
initonly Fields Literal fields are a convenient way of introducing constants into a class, but they have the limitation that their values must be defined when you compile the program. C++/CLI also provides initonly fields in a class that are constants that you can initialize in a constructor. Here’s an example of an initonly field in a skeleton version of the Length class: value class Length { private: int feet; int inches; public: initonly int inchesPerFoot; // Constructor Length(int ft, int ins) : feet(ft), inches(ins), inchesPerFoot(12) {} };
394
// initonly field
// Initialize fields // Initialize initonly field
Defining Your Own Data Types Here the initonly field has the name inchesPerFoot and is initialized in the initializer list for the constructor. This is an example of a non-static initonly field and each object will have its own copy, just like the ordinary fields, feet and inches. Of course, the big difference between initonly fields and ordinary fields is that you cannot subsequently change the value of an initonly field — after it has been initialized, it is fixed for all time. Note that you must not specify an initial value for a non-static initonly field when you declare it; this implies that you must initialize all non-static initonly fields in a constructor. You don’t have to initialize non-static initonly fields in the constructor’s initializer list — you could do it in the body of the constructor: Length(int ft, int ins) : feet(ft), inches(ins), { inchesPerFoot = 12; }
// Initialize fields // Initialize initonly field
Now the field is initialized in the body of the constructor. You would typically pass the value for a nonstatic initonly field as an argument to the constructor rather than use an explicit literal as we have done here because the point of such fields is that their values are instance specific. If the value is known when you write the code you might as well use a literal field. You can also define an initonly field in a class to be static, in which case it is shared between all members of the class and if it is a public initonly field, it’s accessible by qualifying the field name with the class name. The inchesPerFoot field would make much more sense as a static initonly field — the value really isn’t going to vary from one object to another. Here’s a new version of the Length class with a static initonly field: value class Length { private: int feet; int inches; public: initonly static int inchesPerFoot = 12; // Constructor Length(int ft, int ins) : feet(ft), inches(ins) {}
// Static initonly field
// Initialize fields
};
Now the inchesPerFoot field is static, and it has its value specified in the declaration rather than in the constructor initializer list. Indeed, you are not permitted to set values for static fields of any kind in the constructor. If you think about it, this makes sense because static fields are shared among all objects of the class and therefore setting values for such fields each time a constructor is called would conflict with this notion. You now seem to be back with initonly fields only being initialized at compile time where literal fields could do the job anyway; however, you have another way to initialize static initonly fields at runtime — through a static constructor.
395
Chapter 7
Static Constructors A static constructor is a constructor that you declare using the static keyword and that you use to initialize static fields and static initonly fields. A static constructor has no parameters and cannot have an initializer list. A static constructor is always private, regardless of whether or not you put it in a public section of the class. You can define a static constructor for value classes and for reference classes. You cannot call a static constructor directly — it will be called automatically prior to the execution of a normal constructor. Any static fields that have initial values specified in their declarations will be initialized prior to the execution of the static constructor. Here’s how you could initialize the initonly field in the Length class using a static constructor: value class Length { private: int feet; int inches; // Static constructor static Length() { inchesPerFoot = 12; } public: initonly static int inchesPerFoot; // Constructor Length(int ft, int ins) : feet(ft), inches(ins) {}
// Static initonly field
// Initialize fields
}
This example of using a static constructor has no particular advantage over explicit initialization of inchesPerFoot, but bear in mind that the big difference is that the initialization is now occurring at runtime and the value could be acquired from an external source.
Summar y You now understand the basic ideas behind classes in C++. You’re going to see more and more about using classes throughout the rest of the book. The key points to keep in mind from this chapter are:
396
❑
A class provides a means of defining your own data types. They can reflect whatever types of objects your particular problem requires.
❑
A class can contain data members and function members. The function members of a class always have free access to the data members of the same class. Data members of a C++/CLI class are referred to as fields.
❑
Objects of a class are created and initialized using functions called constructors. These are automatically called when an object declaration is encountered. Constructors may be overloaded to provide different ways of initializing an object.
❑
Classes in a C++/CLI program can be value classes or ref class.
Defining Your Own Data Types ❑
Variables of a value class type store data directly whereas variables referencing ref class objects are always handles.
❑
C++/CLI classes can have a static constructor defined that initializes the static members of a class.
❑
Members of a class can be specified as public, in which case they are freely accessible by any function in a program. Alternatively, they may be specified as private, in which case they may only be accessed by member functions or friend functions of the class.
❑
Members of a class can be defined as static. Only one instance of each static member of a class exists, which is shared amongst all instances of the class, no matter how many objects of the class are created.
❑
Every non-static object of a class contains the pointer this, which points to the current object for which the function was called.
❑
In non-static function members of a value class type, the this pointer is an interior pointer whereas in a ref class type it is a handle.
❑
A member function that is declared as const has a const this pointer, and therefore cannot modify data members of the class object for which it is called. It also cannot call another member function that is not const.
❑
You can only call const member functions for a class object declared as const.
❑
Function members of value classes and ref classes cannot be declared as const.
❑
Using references to class objects as arguments to function calls can avoid substantial overheads in passing complex objects to a function.
❑
A copy constructor, which is a constructor for an object initialized with an existing object of the same class, must have its parameter specified as a const reference.
❑
You cannot define a copy constructor in a value class because copying value class objects is always done by member-by-member copying.
Exercises You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.
Define a struct Sample that contains two integer data items. Write a program which declares two object of type Sample, called a and b. Set values for the data items that belong to a and then check that you can copy the values into b by simple assignment.
2.
Add a char* member to struct Sample in the previous exercise called sPtr. When you fill in the data for a, dynamically create a string buffer initialized with “Hello World!” and make a.sptr point to it. Copy a into b. What happens when you change the contents of the character buffer pointed to by a.sPtr and then output the contents of the string pointed to by b.sPtr? Explain what is happening. How would you get around this?
397
Chapter 7 3.
Create a function which takes a pointer to an object of type Sample as an argument, and which outputs the values of the members of any object Sample that is passed to it. Test this function by extending the program that you created for the previous exercise.
4.
Define a class CRecord with two private data members that store a name up to 14 characters long and an integer item number. Define a getRecord() function member of the CRecord class that will set values for the data members by reading input from the keyboard and a putRecord() function member that outputs the values of the data members. Implement the getRecord() function so that a calling program can detect when a zero item number is entered. Test your CRecord class with a main() function that reads and outputs CRecord objects until a zero item number is entered.
5.
Write a class called CTrace that you can use to show you at run-time when code blocks have been entered and exited, by producing output like this: function ‘f1’ entry ‘if’ block entry ‘if’ block exit function ‘f1’ exit
6.
Can you think of a way to automatically control the indentation in the last exercise, so that the output looks like this? function ‘if’ ‘if’ function
398
‘f1’ entry block entry block exit ‘f1’ exit
7.
Define a class to represent a push-down stack of integers. A stack is a list of items that permits adding (‘pushing’) or removing (‘popping’) items only from one end and works on a last-in, first-out principle. For example, if the stack contained [10 4 16 20], pop() would return 10, and the stack would then contain [4 16 20]; a subsequent push(13) would leave the stack as [13 4 16 20]. You can’t get at an item that is not at the top without first popping the ones above it. Your class should implement push() and pop() functions, plus a print() function so that you can check the stack contents. Store the list internally as an array, for now. Write a test program to verify the correct operation of your class.
8.
What happens with your solution to the previous exercise if you try to pop() more items than you’ve pushed, or save more items than you have space for? Can you think of a robust way to trap this? Sometimes you might want to look at the number at the top of the stack without removing it; implement a peek() function to do this.
9.
Repeat Ex7-4 but as a CLR console program using ref classes.
8 More on Classes In this chapter, you will extend your knowledge of classes by understanding how you can make your class objects work more like the basic types in C++. You will learn about: ❑
Class destructors and when and why they are necessary
❑
How to implement a class destructor
❑
How to allocate data members of a native C++ class in the free store and how to delete them when they are no longer required
❑
When you must write a copy constructor for a class
❑
Unions and how they can be used
❑
How to make objects of your class work with C++ operators such as + or *
❑
Class templates and how you define and use them
❑
How to overload operators in C++/CLI classes
Class Destructors Although this section heading refers to destructors, it’s also about dynamic memory allocation. When you allocate memory in the free store for class members, you are invariably obliged to make use of a destructor, in addition to a constructor of course, and, as you’ll see later in this chapter, using dynamically allocated class members also require you to write your own copy constructor.
What Is a Destructor? A destructor is a function that destroys an object when it is no longer required or when it goes out of scope. The class destructor is called automatically when an object goes out of scope. Destroying an object involves freeing the memory occupied by the data members of the object (except for static members which continue to exist even when there are no class objects in existence). The destructor for a class is a member function with the same name as the class, preceded by a tilde
Chapter 8 (~). The class destructor doesn’t return a value and doesn’t have parameters defined. For the CBox class, the prototype of the class destructor is: ~CBox();
// Class destructor prototype
Because a destructor has no parameters, there can only ever be one destructor in a class. It’s an error to specify a return value or parameters for a destructor.
The Default Destructor All the objects that you have been using up to now have been destroyed automatically by the default destructor for the class. The default destructor is always generated automatically by the compiler if you do not define your own class destructor. The default destructor doesn’t delete objects or object members that have been allocated in the free store by the operator new. If space for class members has been allocated dynamically in a contructor, you must define your own destructor that explicitly use the delete operator to release the memory that has been allocated by the constructor using the operator new, just as you would with ordinary variables. You need some practice in writing destructors, so let’s try it out.
Try It Out
A Simple Destructor
To get an appreciation of when the destructor for a class is called, you can include a destructor in the class CBox. Here’s the definition of the example including the CBox class with a destructor: // Ex8_01.cpp // Class with an explicit destructor #include using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Destructor definition ~CBox() { cout << “Destructor called.” << endl; } // Constructor definition CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv) { cout << endl << “Constructor called.”; } // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } // Function to compare two boxes which returns true
400
More on Classes // if the first is greater that the second, and false otherwise int compare(CBox* pBox) const { return this->Volume() > pBox->Volume(); } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; // Function to demonstrate the CBox class destructor in action int main() { CBox boxes[5]; // Array of CBox objects declared CBox cigar(8.0, 5.0, 1.0); // Declare cigar box CBox match(2.2, 1.1, 0.5); // Declare match box CBox* pB1 = &cigar; // Initialize pointer to cigar object address CBox* pB2 = 0; // Pointer to CBox initialized to null cout << endl << “Volume of cigar is “ << pB1->Volume(); // Volume of obj. pointed to pB2 = boxes; // Set to address of array boxes[2] = match; // Set 3rd element to match cout << endl << “Volume of boxes[2] is “ << (pB2 + 2)->Volume(); // Now access thru pointer cout << endl; return 0; }
How It Works The only thing that the CBox class destructor does is to display a message showing that it was called. The output is: Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Constructor called. Volume of cigar is 40 Volume of boxes[2] is 1.21 Destructor called. Destructor called. Destructor called. Destructor called. Destructor called. Destructor called. Destructor called.
401
Chapter 8 You get one call of the destructor at the end of the program for each of the objects that exist. For each constructor call that occurred, there’s a matching destructor call. You don’t need to call the destructor explicitly here. When an object of a class goes out of scope, the compiler arranges for the destructor for the class to be called automatically. In our example, the destructor calls occur after main() has finished executing, so it’s quite possible for an error in a destructor to cause a program to crash after main() has safely terminated.
Destructors and Dynamic Memory Allocation You will find that you often want to allocate memory for class data members dynamically. You can use the operator new in a constructor to allocate space for an object member. In such a case, you must assume responsibility for releasing the space when the object is no longer required by providing a suitable destructor. Let’s first define a simple class where we can do this. Suppose you want to define a class where each object is a message of some description, for example, a text string. The class should be as memory efficient as possible so, rather than defining a data member as a char array big enough to hold the maximum length string that you might require, you’ll allocate memory in the free store for the message when an object is created. Here’s the class definition: //Listing 08_01 class CMessage { private: char* pmessage;
// Pointer to object text string
public: // Function to display a message void ShowIt() const { cout << endl << pmessage; } // Constructor definition CMessage(const char* text = “Default message”) { pmessage = new char[strlen(text) + 1]; // Allocate space for text strcpy(pmessage, text); // Copy text to new memory } ~CMessage();
// Destructor prototype
};
This class has only one data member defined, pmessage, which is a pointer to a text string. This is defined in the private section of the class, so that it can’t be accessed from outside the class. In the public section, you have the ShowIt() function that outputs a CMessage object to the screen. You also have the definition of a constructor and you have the prototype for the class destructor, ~CMessage(), which I’ll come to in a moment.
402
More on Classes The constructor for the class requires a string as an argument, but if none is passed, it uses the default string that is specified for the parameter. The constructor obtains the length of the string supplied as the argument, excluding the terminating NULL, by using the library function strlen(). For the constructor to use this library function, there must be a #include statement for the header file. The constructor determines the number of bytes of memory necessary to store the string in the free store by adding 1 to the value that the function strlen() returns. Of course, if the memory allocation fails, an exception will be thrown that will terminate the program. If you want to manage such a failure to provide a more graceful end to the program, you would catch the exception within the constructor code. (See Chapter 6 for information on handling out-of-memory conditions.) Having obtained the memory for the string using the operator new, you use the strcpy() library function that is also declared in the header file to copy the string supplied as the argument to the constructor into the memory allocated for it. The strcpy() function copies the string specified by the second pointer argument to the address contained in the first pointer argument. You now need to write a class destructor that frees up the memory allocated for a message. If you don’t provide a destructor for the class, there’s no way to delete the memory allocated for an object. If you use this class as it stands in a program where a large number of CMessage objects are created, the free store is gradually eaten away until the program fails. It’s easy for this to occur in circumstances where it may not be obvious that it is happening. For example, if you create a temporary CMessage object in a function that is called many times in a program, you might assume that the objects are being destroyed at the return from the function. You’d be right about that, of course, but the free store memory is released. Thus for each call of the function, more of the free store is occupied by memory for discarded CMessage objects. The code for the CMessage class destructor is as follows: // Listing 08_02 // Destructor to free memory allocated by new CMessage::~CMessage() { cout << “Destructor called.” // Just to track what happens << endl; delete[] pmessage; // Free memory assigned to pointer }
Because you’re defining the destructor outside of the class definition, you must qualify the name of the destructor with the class name, CMessage. All the destructor does is display a message so that you can see what’s going on and then use the delete operator to free the memory pointed to by the member pmessage. Note that you have to include the square brackets with delete because you’re deleting an array (of type char).
Try It Out
Using the Message Class
You can exercise the CMessage class with a little example: // Ex8_02.cpp // Using a destructor to free memory #include // For stream I/O
403
Chapter 8 #include using std::cout; using std::endl;
// For strlen() and strcpy()
// Put the CMessage class definition here (Listing 08_01) // Put the destructor definition here (Listing 08_02) int main() { // Declare object CMessage motto(“A miss is as good as a mile.”); // Dynamic object CMessage* pM = new CMessage(“A cat can look at a queen.”); motto.ShowIt(); pM->ShowIt(); cout << endl;
// Display 1st message // Display 2nd message
// delete pM; return 0;
// Manually delete object created with new
}
Don’t forget to replace the comments in the code with the CMessage class and destructor definitions from the previous section; it won’t compile without this.
How It Works At the beginning of main(), you declare and define an initialized CMessage object, motto, in the usual manner. In the second declaration you define a pointer to a CMessage object, pM, and allocate memory for the CMessage object that is pointed to by using the operator new. The call to new invokes the CMessage class constructor, which has the effect of calling new again to allocate space for the message text pointed to by the data member pmessage. If you build and execute this example, it produces the following output: A miss is as good as a mile. A cat can look at a queen. Destructor called.
You have only one destructor call recorded in the output, even though you created two CMessage objects. I said earlier that the compiler doesn’t take responsibility for objects created in the free store. The compiler arranged to call your destructor for the object motto because this is a normal automatic object, even though the memory for the data member was allocated in the free store by the constructor. The object pointed to by pM is different. You allocated memory for the object in the free store, so you have to use delete to remove it. You need to uncomment the following statement that appears just before the return statement in main(): // delete pM;
404
// Manually delete object created with new
More on Classes If you run the code now, it produces this output: A miss is as good as a mile. A cat can look at a queen. Destructor called. Destructor called.
Now you get an extra call of your destructor. This is surprising in a way. Clearly, delete is only dealing with the memory allocated by the call to new in the function main(). It only freed the memory pointed to by pM. Because your pointer pM points to a CMessage object (for which a destructor has been defined), delete also calls your destructor to allow you to release the memory for the members of the object. So when you use delete for an object created dynamically with new, it always calls the destructor for the object before releasing the memory that the object occupies.
Implementing a Copy Constructor When you allocate space for class members dynamically, there are demons lurking in the free store. For the CMessage class, the default copy constructor is woefully inadequate. Suppose you write these statements: CMessage motto1(“Radiation fades your genes.”); CMessage motto2(motto1); // Calls the default copy constructor
The effect of the default copy constructor is to copy the address that is stored in the pointer member of the class from motto1 to motto2 because the copying process implemented by the default copy constructor involves simply copying the values stored in the data members of the original object to the new object. Consequently, only one text string is shared between the two objects, as Figure 8-1 illustrates. If the string is changed from either of the objects, it changes for the other as well because both objects share the same string. If motto1 is destroyed, the pointer in motto2 is pointing at a memory area that has been released, and may now be used for something else, so chaos will surely ensue. Of course, the same problem arises if motto2 is deleted; motto1 would then contain a member pointing to a nonexistent string. The solution is to supply a class copy constructor to replace the default version. You could implement this in the public section of the class as follows: CMessage(const CMessage& initM) // Copy Constructor definition { // Allocate space for text pmessage = new char[ strlen(initM.pmessage) + 1 ]; // Copy text to new memory strcpy(pmessage, initM.pmessage); }
405
Chapter 8 CMessage motto1 ( “ Radiation fades your genes .” ) ; motto1 pmessage address
Radiation fades your genes.
CMessage motto2 (motto1) ; the default copy constructor
// Calls motto2
motto1
copy pmessage address
pmessage address
Radiation fades your genes.
Figure 8-1
Remember from the previous chapter that, to avoid an infinite spiral of calls to the copy constructor, the parameter must be specified as a const reference. This copy constructor first allocates enough memory to hold the string in the object initM, storing the address in the data member of the new object, and then copies the text string from the initializing object. Now, the new object is identical to, but quite independent of, the old one. Just because you don’t initialize one CMessage class object with another, don’t think that you’re safe and need not bother with the copy constructor. Another monster lurks in the free store that can emerge to bite you when you least expect it. Consider the following statements: CMessage thought(“Eye awl weighs yews my spell checker.”); DisplayMessage(thought); // Call a function to output a message
where the function DisplayMessage() is defined as: void DisplayMessage(CMessage localMsg) { cout << endl << “The message is: “ << localMsg.ShowIt(); return; }
406
More on Classes Looks simple enough, doesn’t it? What could be wrong with that? A catastrophic error, that’s what! What the function DisplayMessage() does is actually irrelevant. The problem lies with the parameter. The parameter is a CMessage object so the argument in a call is passed by value. With the default copy constructor, the sequence of events is as follows:
1.
The object thought is created with the space for the message “Eye awl weighs yews my spell checker” allocated in the free store.
2.
The function DisplayMessage() is called and, because the argument is passed by value, a copy, localMsg, is made using the default copy constructor. Now the pointer in the copy points to the same string in the free store as the original object.
3.
At the end of the function, the local object goes out of scope, so the destructor for the CMessage class is called. This deletes the local object (the copy) by deleting the memory pointed to by the pointer pmessage.
4.
On return from the function DisplayMessage(), the pointer in the original object, thought, still points to the memory area that has just been deleted. Next time you try to use the original object (or even if you don’t, since it needs to be deleted sooner or later) your program will behave in weird and mysterious ways.
Any call to a function that passes by value an object of a class that has a member defined dynamically will cause problems. So, out of this, you have an absolutely 100 percent, 24 carat golden rule: If you allocate space for a member of a native C++ class dynamically, always implement a copy constructor.
Sharing Memor y Between Variables As a relic of the days when 64K was quite a lot of memory, you have a facility in C++ which allows more than one variable to share the same memory (but obviously not at the same time). This is called a union, and there are four basic ways in which you can use one: ❑
You can use it so that a variable A occupies a block of memory at one point in a program, which is later occupied by another variable B of a different type, because A is no longer required. I recommend that you don’t do this. It’s not worth the risk of error that is implicit in such an arrangement. You can achieve the same effect by allocating memory dynamically.
❑
You could have a situation in a program where a large array of data is required, but you don’t know in advance of execution what the data type will be — it will be determined by the input data. I also recommend that you don’t use unions in this case because you can achieve the same result using a couple of pointers of different types and again allocating the memory dynamically.
❑
A third possible use for a union is the one that you may need now and again — when you want to interpret the same data in two or more different ways. This could happen when you have a variable that is of type long, and you want to treat it as two values of type short. Windows sometimes packages two short values in a single parameter of type long passed to a function. Another instance arises when you want to treat a block of memory containing numeric data as a string of bytes, just to move it around.
407
Chapter 8 ❑
You can use a union as a means of passing an object or a data value around where you don’t know in advance what its type is going to be. The union can provide for storing any one of the possible range of types that you might have.
Defining Unions You define a union using the keyword union. It is best understood by taking an example of a definition: union shareLD { double dval; long lval; };
// Sharing memory between long and double
This defines a union type shareLD that provides for the variables of type long and double to occupy the same memory. The union type name is usually referred to as a tag name. This statement is similar to a class definition in that you haven’t actually defined a union instance yet, so you don’t have any variables at this point. After the union type has been defined, you can define instances of a union in a declaration. For example: shareLD myUnion;
This defined an instance of the union type, shareLD, that you defined previously. You could also have defined myUnion by including it in the union definition statement: union shareLD { double dval; long lval; } myUnion;
// Sharing memory between long and double
To refer to a member of the union, you use the direct member selection operator (the period) with the union instance name, just as you have done when accessing members of a class. So, you could set the long variable lval to 100 in the union instance MyUnion with this statement: myUnion.lval = 100;
// Using a member of a union
Using a similar statement later in a program to initialize the double variable dval overwrites lval. The basic problem with using a union to store different types of values in the same memory is that because of the way a union works, you also need some means of determining which of the member values is current. This is usually achieved by maintaining another variable that acts as an indicator of the type of value stored. A union is not limited to sharing between two variables. If you want, you can share the same memory between several variables. The memory occupied by the union is that required by its largest member. For example, suppose you define this union: union shareDLF { double dval;
408
More on Classes long lval; float fval; } uinst = {1.5};
An instance of shareDLF occupies 8 bytes, as illustrated in Figure 8-2. 8 bytes
Ival
fval
dval
Figure 8-2
In the example, you defined an instance of the union, uinst, as well as the tag name for the union. You also initialized the instance with the value 1.5. You can only initialize the first member of the union when you declare an instance.
Anonymous Unions You can define a union without a union type name, in which case an instance of the union is automatically declared. For example, suppose you define a union like this: union { char* pval; double dval; long lval; };
This statement defines both a union with no name and an instance of the union with no name. Consequently, you can refer the variables that it contains just by their names, as they appear in the union definition, pval, dval, and lval. This can be more convenient than a normal union with a type name, but you need to be careful that you don’t confuse the union members with ordinary variables. The members of the union still share the same memory. As an illustration of how the anonymous union above works, to use the double member, you could write this statement: dval = 99.5;
// Using a member of an anonymous union
409
Chapter 8 As you can see, there’s nothing to distinguish the variable dval as a union member. If you need to use anonymous unions, you could use a naming convention to make the members more obvious and thus make your code a little safer from being misunderstood.
Unions in Classes and Structures You can include an instance of a union in a class or in a structure. If you intend to store different types of value at different times, this usually necessitates maintaining a class data member to indicate what kind of value is stored in the union. There isn’t usually a great deal to be gained by using unions as class or struct members.
Operator Overloading Operator overloading is a very important capability because it enables you to make standard C++ operators, such as +, -, * and so on, work with objects of your own data types. It allows you to write a function that redefines a particular operator so that it performs a particular action when it’s used with objects of a class. For example, you could redefine the operator > so that, when it was used with objects of the class CBox that you saw earlier, it would return true if the first CBox argument had a greater volume than the second. Operator overloading doesn’t allow you to invent new operators, nor can you change the precedence of an operator, so your overloaded version of an operator has the same priority in the sequence of evaluating an expression as the original base operator. The operator precedence table can be found in Chapter 2 of this book and in the MSDN Library. Although you can’t overload all the operators, the restrictions aren’t particularly oppressive. The following are the operators that you can’t overload: The scope resolution operator
::
The conditional operator
?:
The direct member selection operator
.
The size-of operator
sizeof
The de-reference pointer to class member operator
.*
Anything else is fair game, which gives you quite a bit of scope. Obviously, it’s a good idea to ensure that your versions of the standard operators are reasonably consistent with their normal usage, or at least reasonably intuitive in their operation. It wouldn’t be a very sensible approach to produce an overloaded + operator for a class that performed the equivalent of a multiply on class objects. The best way to understand how operator overloading works is to work through an example, so let’s implement what I just referred to, the greater than operator, >, for the CBox class.
410
More on Classes
Implementing an Overloaded Operator To implement an overloaded operator for a class, you have to write a special function. Assuming that it is a member of the class CBox, the declaration for the function to overload the > operator within the class definition is as follows: class CBox { public: bool operator>(CBox& aBox) const; // Rest of the class definition... };
// Overloaded ‘greater than’
The word operator here is a keyword. Combined with an operator symbol or name, in this case, >, it defines an operator function. The function name in this case is operator>(). You can write an operator function with or without a space between the keyword operator and the operator itself, as long as there’s no ambiguity. The ambiguity arises with operators with names rather than symbols such as new or delete. If you were to write operatornew and operatordelete without a space, they are legal names for ordinary functions, so for operator functions with these operators, you must leave a space between the keyword operator and the operator name itself. Note that you declare the operator>() function as const because it doesn’t modify the data members of the class. With the operator>()operator function, the right operand of the operator is defined by the function parameter. The left operand is defined implicitly by the pointer this. So, if you have the following if statement: if(box1 > box2) cout << endl << “box1 is greater than box2”;
the expression between the parentheses in the if will call our operator function and is equivalent to this function call: box1.operator>(box2);
The correspondence between the CBox objects in the expression and the operator function parameters is illustrated in Figure 8-3. if( box1 > box2 )
Function argument
bool CBox::operator>(const CBox& aBox) const { The object pointed to by this
return (this->volume()) > (aBox.Volume()); }
Figure 8-3
411
Chapter 8 Let’s look at how the code for the operator>() function works: // Operator function for ‘greater than’ which // compares volumes of CBox objects. bool CBox::operator>(const CBox& aBox) const { return this->Volume() > aBox.Volume(); }
You use a reference parameter to the function to avoid unnecessary copying when the function is called. Because the function does not alter the object for which it is called, you can declare it as const. If you don’t do this, you could not use the operator to compare const objects of type CBox at all. The return expression uses the member function Volume() to calculate the volume of the CBox object pointed to by this, and compares the result with the volume of the object aBox using the basic operator > . The basic > operator returns a value of type int (not a bool) and thus, 1 is returned if the CBox object pointed to by the pointer this has a larger volume than the object aBox passed as a reference argument, and 0 otherwise. The value that results from the comparison will be automatically converted to the return type of the operator function, type bool.
Try It Out
Operator Overloading
You can exercise the operator>() function with an example: // Ex8_03.cpp // Exercising the overloaded ‘greater than’ operator #include // For stream I/O using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv) { cout << endl << “Constructor called.”; } // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } bool operator>(const CBox& aBox) const; // Destructor definition ~CBox() { cout << “Destructor called.” << endl;
412
// Overloaded ‘greater than’
More on Classes } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; // Operator function for ‘greater than’ that // compares volumes of CBox objects. bool CBox::operator>(const CBox& aBox) const { return this->Volume() > aBox.Volume(); } int main() { CBox smallBox(4.0, 2.0, 1.0); CBox mediumBox(10.0, 4.0, 2.0); CBox bigBox(30.0, 20.0, 40.0); if(mediumBox > smallBox) cout << endl << “mediumBox is bigger than smallBox”; if(mediumBox > bigBox) cout << endl << “mediumBox is bigger than bigBox”; else cout << endl << “mediumBox is not bigger than bigBox”; cout << endl; return 0; }
How It Works The prototype of the operator>() operator function appears in the public section of the class. As the function definition is outside the class definition, it won’t default to inline. This is quite arbitrary. You could just as well have put the definition in place of the prototype in the class definition. In this case, you wouldn’t need to qualify the function name with CBox:: in front of it. As you’ll remember, this is necessary when a function member is defined outside the class definition to tell the compiler that the function is a member of the class CBox. The function main() has two if statements using the operator > with class members. These automatically invoke our overloaded operator. If you wanted to get confirmation of this, you could add an output statement to the operator function. The output from this example is: Constructor called. Constructor called. Constructor called. mediumBox is bigger than smallBox
413
Chapter 8 mediumBox is not bigger than bigBox Destructor called. Destructor called. Destructor called.
The output demonstrates that the if statements work fine with our operator function, so being able to express the solution to CBox problems directly in terms of CBox objects is beginning to be a realistic proposition.
Implementing Full Support for an Operator With our operator function operator>(), there are still a lot of things that you can’t do. Specifying a problem solution in terms of CBox objects might well involve statements such as the following: if(aBox > 20.0) // Do something...
Our function won’t deal with that. If you try to use an expression comparing a CBox object with a numerical value, you’ll get an error message. To support this capability, you would need to write another version of the operator>()function as an overloaded function. You can quite easily support the type of expression that you’ve just seen. The declaration of the member function within the class would be: // Compare a CBox object with a constant bool operator>(const double& value) const;
This would appear in the definition of the class and the right operand for the > operator corresponds to the function parameter here. The CBox object that is the left operand is passed as the implicit pointer this. The implementation of this overloaded operator is also easy. It’s just one statement in the body of the function: // Function to compare a CBox object with a constant bool CBox::operator>(const double& value) const { return this->Volume() > value; }
This couldn’t be much simpler, could it? But you still have a problem using the > operator with CBox objects. You may well want to write statements such as this: if(20.0 > aBox) // do something...
You might argue that this could be done by implementing the operator<() operator function that accepted a right argument of type double and rewriting the statement above to use it, which is quite true. Indeed, implementing the < operator is likely to be a requirement for comparing CBox objects anyway, but an implementation of support for an object type shouldn’t artificially restrict the ways in which
414
More on Classes you can use the objects in an expression. The use of the objects should be as natural as possible. The problem is how to do it. A member operator function always provides the left argument as the pointer this. Because the left argument, in this case, is of type double, you can’t implement it as a member function. That leaves you with two choices: an ordinary function or a friend function. Because you don’t need to access the private members of the class, it doesn’t need to be a friend function, so you can implement the overloaded > operator with a left operand of type double as an ordinary function. The prototype, placed outside the class definition, of course, because it isn’t a member, is: bool operator>(const double& value, const CBox& aBox);
The implementation is: // Function comparing a constant with a CBox object bool operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); }
As you have seen already, an ordinary function (and a friend function too for that matter) accesses the members of an object by using the direct member selection operator and the object name. Of course, an ordinary function only has access to the public members. The member function Volume() is public, so there’s no problem using it here. If the class didn’t have the public function Volume(), you could either use a friend function that could access the private data members directly, or you could provide a set of member functions to return the values of the private data members and use those in an ordinary function to implement the comparison.
Try It Out
Complete Overloading of the > Operator
We can put all this together in an example to show how it works: // Ex8_04.cpp // Implementing a complete overloaded ‘greater than’ operator #include // For stream I/O using std::cout; using std::endl; class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv) { cout << endl << “Constructor called.”; } // Function to calculate the volume of a box
415
Chapter 8 double Volume() const { return m_Length*m_Width*m_Height; } // Operator function for ‘greater than’ that // compares volumes of CBox objects. bool operator>(const CBox& aBox) const { return this->Volume() > aBox.Volume(); } // Function to compare a CBox object with a constant bool operator>(const double& value) const { return this->Volume() > value; } // Destructor definition ~CBox() { cout << “Destructor called.” << endl;} private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int operator>(const double& value, const CBox& aBox); // Function prototype int main() { CBox smallBox(4.0, 2.0, 1.0); CBox mediumBox(10.0, 4.0, 2.0); if(mediumBox > smallBox) cout << endl << “mediumBox is bigger than smallBox”; if(mediumBox > 50.0) cout << endl << “mediumBox capacity is more than 50”; else cout << endl << “mediumBox capacity is not more than 50”; if(10.0 > cout << << else cout << <<
smallBox) endl “smallBox capacity is less than 10”; endl “smallBox capacity is not less than 10”;
cout << endl;
416
More on Classes return 0; } // Function comparing a constant with a CBox object int operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); }
How It Works Note the position of the prototype for the ordinary function version of operator>(). It needs to follow the class definition because it refers to a CBox object in the parameter list. If you place it before the class definition, the example will not compile. There is a way to place it at the beginning of the program file following the #include statement: use an incomplete class declaration. This would precede the prototype and would look like this: class CBox; int operator>(const double& value, CBox& aBox);
// Incomplete class declaration // Function prototype
The incomplete class declaration identifies CBox to the compiler as a class and is sufficient to allow the compiler to process the prototype for the function properly because the compiler now knows that CBox is a user-defined type to be specified later. This mechanism is also essential in circumstances such as those where you have two classes, each of which has a pointer to an object of the other class as a member. They each require the other to be declared first. It’s possible to resolve such an impasse through the use of an incomplete class declaration. The output from the example is: Constructor called. Constructor called. mediumBox is bigger than smallBox mediumBox capacity is more than 50 smallBox capacity is less than 10 Destructor called. Destructor called.
After the constructor messages due to the declarations of the objects smallBox and mediumBox, you have the output lines from the three if statements, each of which is working as you would expect. The first of these calls the operator function that is a class member and works with two CBox objects. The second calls the member function that has a parameter of type double. The expression in the third if statement calls the operator function that you have implemented as an ordinary function. As it happens, you could have made both the operator functions that are class members ordinary functions because they only need access to the member function Volume(), which is public. Any comparison operator can be implemented in much the same way as you have implemented these. They would only differ in the minor details and the general approach to implementing them would be exactly the same.
417
Chapter 8
Overloading the Assignment Operator If you don’t provide an overloaded assignment operator function for your class, the compiler provides a default. The default version simply provides a member-by-member copying process, similar to that of the default copy constructor; however, don’t confuse the default copy constructor with the default assignment operator. The default copy constructor is called by a declaration of a class object that’s initialized with an existing object of the same class or by passing an object to a function by value. The default assignment operator, on the other hand, is called when the left side and the right side of an assignment statement are objects of the same class type. For the CBox class, the default assignment operator works with no problem, but for any class that has space for members allocated dynamically, you need to look carefully at the requirements of the class in question. There may be considerable potential for chaos in your program if you leave the assignment operator out under these circumstances. For a moment, return to the CMessage class that you used when I was talking about copy constructors. Remember it had a member, pmessage, that was a pointer to a string. Now consider the effect that the default assignment operator could have. Suppose you had two instances of the class, motto1 and motto2. You could try setting the members of motto2 equal to the members of motto1 using the default assignment operator, as follows: motto2 = motto1;
// Use default assignment operator
The effect of using the default assignment operator for this class is essentially the same as using the default copy constructor: disaster will result! Because each object has a pointer to the same string, if the string is changed for one object, it’s changed for both. There’s also the problem that when one of the instances of the class is destroyed, its destructor frees the memory used for the string and the other object is left with a pointer to memory that may now be used for something else. What you need the assignment operator to do is to copy the text to a memory area owned by the destination object.
Fixing the Problem You can fix this with your own assignment operator function, which we will assume is defined within the class definition: // Overloaded assignment operator for CMessage objects CMessage& operator=(const CMessage& aMess) { // Release memory for 1st operand delete[] pmessage; pmessage = new char[ strlen(aMess.pmessage) + 1]; // Copy 2nd operand string to 1st strcpy(this->pmessage, aMess.pmessage); // Return a reference to 1st operand return *this; }
418
More on Classes An assignment might seem very simple, but a couple of subtleties need further investigation. Note that you return a reference from the assignment operator function. It may not be immediately apparent why this is so(after all, the function does complete the assignment operation entirely, and the object on the right of the assignment is copied to that on the left. Superficially this would suggest that you don’t need to return anything, but you need to consider how the operator might be used in a little more depth. There’s a possibility that you might need to use the result of an assignment operation on the right side of an expression. Consider a statement such as this: motto1 = motto2 = motto3;
Because the assignment operator is right-associative, the assignment of motto3 to motto2 is carried out first, so this translates into the following statement: motto1 = (motto2.operator=(motto3));
The result of the operator function call here is on the right of the equals sign, so the statement finally becomes this: motto1.operator=(motto2.operator=(motto3));
If this is to work, you certainly have to return something. The call of the operator=() function between the parentheses must return an object that can be used as an argument to the other operator=() function call. In this case a return type of either CMessage or CMessage& would do it, so a reference is not mandatory in this situation, but you must at least return a CMessage object. However, consider the following example: (motto1 = motto2) = motto3;
This is perfectly legitimate code(the parentheses serve to make sure the leftmost assignment is carried out first. This translates into the following statement: (motto1.operator=(motto2)) = motto3;
When you express the remaining assignment operation as the explicit overloaded function call, this ultimately becomes: (motto1.operator=(motto2)).operator=(motto3);
Now you have a situation where the object returned from the operator=() function is used to call the operator=() function. If the return type is just CMessage, this is not legal, because a temporary copy of the original object is actually returned, and the compiler does not allow a member function call using a temporary object. In other words, the return value when the return type is CMessage is not an lvalue. The only way to ensure this sort of thing compiles and works correctly is to return a reference, which is an lvalue, so the only possible return type if want to allow fully flexible use of the assignment operator with your class objects is CMessage&. Note that the native C++ language does not enforce any restrictions on the accepted parameter or return types for the assignment operator, but it makes sense to declare the operator in the way I have just described if you want your assignment operator functions to support normal C++ usage of assignment.
419
Chapter 8 The second subtlety you need to keep in mind is that each object already has memory for a string allocated, so the first thing that the operator function has to do is to delete the memory allocated to the first object and reallocate sufficient memory to accommodate the string belonging to the second object. After this is done, the string from the second object can be copied to the new memory now owned by the first. There’s still a defect in this operator function. What if you were to write the following statement? motto1 = motto1;
Obviously, you wouldn’t do anything as stupid as this directly, but it could easily be hidden behind a pointer, for instance, as in the following statement. Motto1 = *pMess;
If the pointer pMess points to motto1 you essentially have the preceding assignment statement. In this case, the operator function as it stands would delete the memory for motto1, allocate some more memory based on the length of the string that has already been deleted and try to copy the old memory which, by then, could well have been corrupted. You can fix this with a check for identical left and right operands at the beginning of the function, so now the definition of the operator=() function would become this: // Overloaded assignment operator for CMessage objects CMessage& operator=(const CMessage& aMess) { if(this == &aMess) // Check addresses, if equal return *this; // return the 1st operand // Release memory for 1st operand delete[] pmessage; pmessage = new char[ strlen(aMess.pmessage) +1]; // Copy 2nd operand string to 1st strcpy(this->pmessage, aMess.pmessage); // Return a reference to 1st operand return *this; }
This code assumes that the function definition appears within the class definition.
Try It Out
Overloading the Assignment Operator
Let’s put this together in a working example. We’ll add a function, called Reset(), to the class at the same time. This just resets the message to a string of asterisks. // Ex8_05.cpp // Overloaded copy operator perfection #include #include using std::cout; using std::endl; class CMessage
420
More on Classes { private: char* pmessage;
// Pointer to object text string
public: // Function to display a message void ShowIt() const { cout << endl << pmessage; } //Function to reset a message to * void Reset() { char* temp = pmessage; while(*temp) *(temp++) = ‘*’; } // Overloaded assignment operator for CMessage objects CMessage& operator=(const CMessage& aMess) { if(this == &aMess) // Check addresses, if equal return *this; // return the 1st operand // Release memory for 1st operand delete[] pmessage; pmessage = new char[ strlen(aMess.pmessage) +1]; // Copy 2nd operand string to 1st strcpy(this->pmessage, aMess.pmessage); // Return a reference to 1st operand return *this; } // Constructor definition CMessage(const char* text = “Default message”) { pmessage = new char[ strlen(text) +1 ]; // Allocate space for text strcpy(pmessage, text); // Copy text to new memory } // Destructor to free memory allocated by new ~CMessage() { cout << “Destructor called.” // Just to track what happens << endl; delete[] pmessage; // Free memory assigned to pointer } };
421
Chapter 8 int main() { CMessage motto1(“The devil takes care of his own”); CMessage motto2; cout << “motto2 contains - “; motto2.ShowIt(); cout << endl; motto2 = motto1;
// Use new assignment operator
cout << “motto2 contains - “; motto2.ShowIt(); cout << endl; motto1.Reset();
// Setting motto1 to * doesn’t // affect motto2
cout << “motto1 now contains - “; motto1.ShowIt(); cout << endl; cout << “motto2 still contains - “; motto2.ShowIt(); cout << endl; return 0; }
You can see from the output of this program that everything works exactly as required, with no linking between the messages of the two objects, except where you explicitly set them equal: motto2 contains Default message motto2 contains The devil takes care of his own motto1 now contains ******************************* motto2 still contains The devil takes care of his own Destructor called. Destructor called.
So let’s have another golden rule out of all of this: Always implement an assignment operator if you allocate space dynamically for a data member of a class. Having implemented the assignment operator, what happens with operations such as +=? Well, they don’t work unless you implement them. For each form of op= that you want to use with your class objects, you need to write another operator function.
422
More on Classes
Overloading the Addition Operator Let’s look at overloading the addition operator for our CBox class. This is interesting because it involves creating and returning a new object. The new object is the sum (whatever you define that to mean) of the two CBox objects that are its operands. So what do we want the sum of two boxes to mean? Well, there are quite a few legitimate possibilities but we’ll keep it simple here. Let’s define the sum of two CBox objects as a CBox object which is large enough to contain the other two boxes stacked on top of each other. You can do this by making the new object have an m_Length member, which is the larger of the m_Length members of the objects being added, and an m_Width member derived in a similar way. The m_Height member is the sum of the m_Height members of the two operand objects, so that the resultant CBox object can contain the other two CBox objects. This isn’t necessarily an optimal solution, but it is sufficient for our purposes. By altering the constructor, we’ll also arrange that the m_Length member of a CBox object is always greater than or equal to the m_Width member. Our version of the addition operation for boxes is easier to explain graphically, so it’s illustrated in Figure 8-4. L = 30
W = 20
maximum length
L = 25
box1 W = 25
H = 15
H = 10
L = 30
maximum width
W = 25
Sum of heights
Figure 8-4
box2
box1+box2
H = 15+10 = 25
423
Chapter 8 Because you need to get at the members of an object directly, you make the operator+() a member function. The declaration of the function member within the class definition is this: CBox operator+(const CBox& aBox) const; // Function adding two CBox objects
You define the parameter as a reference to avoid unnecessary copying of the right argument when the function is called, and you make it a const reference because the function does not modify the argument. If you don’t declare the parameter as a const reference, the compiler does not allow a const object to be passed to the function, so it would then not be possible for the right operand of + to be a const CBox object. You also declare the function as const as it doesn’t change the object for which it is called. Without this, the left operand of + could not be a const CBox object. The operator+() function definition is now as follows: // Function to add two CBox objects CBox CBox::operator+(const CBox& aBox) const { // New object has larger length and width, and sum of heights return CBox(m_Length > aBox.m_Length ? m_Length:aBox.m_Length, m_Width > aBox.m_Width ? m_Width:aBox.m_Width, m_Height + aBox.m_Height); }
You construct a local CBox object from the current object (*this) and the object that is passed as the argument, aBox. Remember that the return process makes a temporary copy of the local object and that is passed back to the calling function, not the local object discarded on return from the function.
Try It Out
Exercising Our Addition
You’ll be able to see how the overloaded addition operator in the CBox class works in this example: // Ex8_06.cpp // Adding CBox objects #include using std::cout; using std::endl;
// For stream I/O
class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Height(hv) { m_Length = lv > wv? lv: wv; // Ensure that m_Width = wv < lv? wv: lv; // length >= width } // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } // Operator function for ‘greater than’ which
424
More on Classes // compares volumes of CBox objects. int CBox::operator>(const CBox& aBox) const { return this->Volume() > aBox.Volume(); } // Function to compare a CBox object with a constant int operator>(const double& value) const { return Volume() > value; } // Function to add two CBox objects CBox operator+(const CBox& aBox) const { // New object has larger length & width, and sum of heights return CBox(m_Length > aBox.m_Length? m_Length:aBox.m_Length, m_Width > aBox.m_Width? m_Width:aBox.m_Width, m_Height + aBox.m_Height); } // Function to show the dimensions of a box void ShowBox() const { cout << m_Length << “ “ << m_Width << “ “ << m_Height << endl; } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
}; int operator>(const double& value, const CBox& aBox); // Function prototype int main() { CBox smallBox(4.0, 2.0, 1.0); CBox mediumBox(10.0, 4.0, 2.0); CBox aBox; CBox bBox; aBox = smallBox + mediumBox; cout << “aBox dimensions are “; aBox.ShowBox(); bBox = aBox + smallBox + mediumBox; cout << “bBox dimensions are “; bBox.ShowBox(); return 0; } // Function comparing a constant with a CBox object
425
Chapter 8 int operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); }
You’ll be using the CBox class definition again a few pages down the road in this chapter, so make a note that you’ll want to return to this point in the book.
How It Works In this example I have changed the CBox class members a little. The destructor has been deleted as it isn’t necessary for this class, and the constructor has been modified to ensure that the m_Length member isn’t less than the m_Width member. Knowing that the length of a box is always at least as big as the width makes the add operation a bit easier. I’ve also added the ShowBox() function to output the dimensions of a CBox object. Using this we’ll be able to verify that our overloaded add operation is working as we expect. The output from this program is: aBox dimensions are 10 4 3 bBox dimensions are 10 4 6
This seems to be consistent with the notion of adding CBox objects that we have defined and, as you can see, the function also works with multiple add operations in an expression. For the computation of bBox, the overloaded addition operator is called twice. You could equally well have implemented the add operation for the class as a friend function. Its prototype is this: friend CBox operator+(const CBox& aBox, const CBox& bBox);
The process for producing the result would be much the same, except that you’d need to use the direct member selection operator to obtain the members for both the arguments to the function. It would work just as well as the first version of the operator function.
Overloading the Increment and Decrement Operators I’ll briefly introduce the mechanism for overloading the increment and decrement operators in a class because they have some special characteristics that make them different from other unary operators. You need a way to deal with the fact that the ++ and -- operators come in a prefix and postfix form, and the effect is different depending on whether the operator is applied in its prefix or postfix form. In native C++ the overloaded operator is different for the prefix and postfix forms of the increment and decrement operators. Here’s how they would be defined in a class with the name Length, for example: class Length { private: double len; public: Length& operator++();
426
// Length value for the class // Prefix increment operator
More on Classes const Length operator++(int);
// Postfix increment operator
Length& operator--(); const Length operator--(int);
// Prefix decrement operator // Postfix decrement operator
// rest of the class... }
This simple class assumes a length is stored just as a value of type double. In reality, you might make a length class more sophisticated than this, but it serves to illustrate how you overload the increment and decrement operators. The primary way the prefix and postfix forms of the overloaded operators are differentiated is by the parameter list; for the prefix form there are no parameters and for the postfix form there is a parameter of type int. The parameter in the postfix operator function is only to distinguish it from the prefix form and is otherwise unused in the function implementation. The prefix increment and decrement operators increment the operand before its value is used in an expression so you just return a reference to the current object after it has been incremented or decremented. With the prefix forms, the operand is incremented after its current value is used in an expression. This is achieved by creating a new object that is a copy of the current object before incrementing the current object and returning the copy after the current object has been modified.
Class Templates You saw in Chapter 6 that you could define a function template that would automatically generate functions varying in the type of arguments accepted, or in the type of value returned. C++ has a similar mechanism for classes. A class template is not in itself a class; it’s a sort of “recipe” for a class that is used by the compiler to generate the code for a class. As you can see from Figure 8-5, it’s like the function template — you determine the class that you want generated by specifying your choice of type for the parameter (T in this case) that appears between the angled brackets in the template. Doing this generates a particular class referred to as an instance of the class template. The process of creating a class from a template is described as instantiating the template. An appropriate class definition is generated when you instantiate an object of a template class for a particular type, so you can generate any number of different classes from one class template. You’ll get a good idea of how this works in practice by looking at an example.
427
Chapter 8 T is a parameter for which you supply an argument value that is a type. Each different type value argument you specify creates a new class.
class CExample { int m_Value; ... }
T specified as int
template class CExample { T m_Value; ... }
T specified as double
class template
T specified as CBox
The class is created by using your value in place of T in the template
class CExample { double m_Value; ... }
class instances
class CExample { CBox m_Value; ... }
Figure 8-5
Defining a Class Template I’ll choose a simple example to illustrate how you define and use a class template, and I won’t complicate things by worrying too much about errors that can arise if it’s misused. Suppose you want to define classes that can store a number of data samples of some kind, and each class is to provide a Max() function to determine the maximum sample value of those stored. This function is similar to the one you saw in the function template discussion in Chapter 6. You can define a class template, which generates a class CSamples to store samples of whatever type you want. template class CSamples { public: // Constructor definition to accept an array of samples CSamples(const T values[], int count) { m_Free = count < 100? count:100; // Don’t exceed the array for(int i = 0; i < m_Free; i++) m_Values[i] = values[i]; // Store count number of samples } // Constructor to accept a single sample CSamples(const T& value) { m_Values[0] = value; // Store the sample
428
More on Classes m_Free = 1;
// Next is free
} // Default constructor CSamples(){ m_Free = 0 }
// Nothing stored, so first is free
// Function to add a sample bool Add(const T& value) { bool OK = m_Free < 100; if(OK) m_Values[m_Free++] = value; return OK; }
// Indicates there is a free place // OK true, so store the value
// Function to obtain maximum sample T Max() const { // Set first sample or 0 as maximum T theMax = m_Free ? m_Values[0] : 0; for(int i = 1; i < m_Free; i++) if(m_Values[i] > theMax) theMax = m_Values[i]; return theMax;
// Check all the samples // Store any larger sample
} private: T m_Values[100]; int m_Free; };
// Array to store samples // Index of free location in m_Values
To indicate that you are defining a template rather than a straightforward class, you insert the template keyword and the type parameter, T, between angled brackets, just before the keyword class and the class name, CSamples. This is essentially the same syntax that you used to define a function template in Chapter 6. The parameter T is the type variable that is replaced by a specific type when you declare a class object. Wherever the parameter T appears in the class definition, it is replaced by the type that you specify in your object declaration; this creates a class definition corresponding to this type. You can specify any type (a basic data type or a class type), but it has to make sense in the context of the class template, of course. Any class type that you use to instantiate a class from a template must have all the operators defined that the member functions of the template will use with such objects. If your class hasn’t implemented operator>(), for example, it does not work with the CSamples class template. In general, you can specify multiple parameters in a class template if you need them. I’ll come back to this possibility a little later in the chapter. Getting back to the example, the type of the array in which the samples are stored is specified as T. The array will therefore be an array of whatever type you specify for T when you declare a CSamples object. As you can see, you also use the type T in two of the constructors for the class, as well as in the Add() and Max() functions. Each of these occurrences is also replaced when you instantiate a class object using the template.
429
Chapter 8 The constructors support the creation of an empty object, an object with a single sample, and an object initialized with an array of samples. The Add() function allows samples to be added to an object one at a time. You could also overload this function to add an array of samples. The class template includes some elementary provision to prevent the capacity of the m_Values array being exceeded in the Add() function, and in the constructor that accepts an array of samples. As I said earlier, in theory you can create objects of CSamples classes that handle any data type: type int, type double, or any class type that you’ve defined. In practice, this doesn’t mean it necessarily compiles and works as you expect. It all depends on what the template definition does, and usually a template only works for a particular range of types. For example, the Max() function implicitly assumes that the > operator is available for whatever type is being processed. If it isn’t, your program does not compile. Clearly, you’ll usually be in the position of defining a template that works for some types but not others, but there’s no way you can restrict what type is applied to a template.
Template Member Functions You may want to place the definition of a class template member function outside of the template definition. The syntax for this isn’t particularly obvious, so let’s look at how you do it. You put the function declaration in the class template definition in the normal way. For instance: template class CSamples { // Rest of the template definition... T Max() const; // Function to obtain maximum sample // Rest of the template definition... }
This declares the Max() function as a member of the class template but doesn’t define it. You now need to create a separate function template for the definition of the member function. You must use the template class name plus the parameters in angled brackets to identify the class template to which the function template belongs: template T CSamples::Max() const { T theMax = m_Values[0]; for(int i = 1; i < m_Free; i++) if(m_Values[i] > theMax) theMax = m_Values[i]; return theMax;
// Set first sample as maximum // Check all the samples // Store any larger sample
}
You saw the syntax for a function template in Chapter 6. Because this function template is for a member of the class template with the parameter T, the function template definition here should have the same parameters as the class template definition. There’s just one in this case — T — but in general there can be several. If the class template had two or more parameters, so would each template defining a member function.
430
More on Classes Note how you only put the parameter name T along with the class name before the scope resolution operator. This is necessary — the parameters are fundamental to the identification of the class to which a function, produced from the template, belongs. The type is CSamples with whatever type you assign to T when you create an instance of the class template. Your type is plugged into the class template to generate the class definition and into the function template to generate the definition for the Max() function for the class. Each class produced from the class template needs to have its own definition for the function Max(). Defining a constructor or a destructor outside of the class template definition is similar. You could write the definition of the constructor that accepts an array of samples as: template CSamples::CSamples(T values[], int count) { m_Free = count < 100? count:100; // Don’t exceed the array for(int i = 0; i < m_Free; i++) m_Values[i] = values[i];
// Store count number of samples
}
The class to which the constructor belongs is specified in the template in the same way as for an ordinary member function. Note that the constructor name doesn’t require the parameter specification(it is just CSamples, but it needs to be qualified by the class template type CSamples. You only use the parameter with the class template name preceding the scope resolution operator.
Creating Objects from a Class Template When you use a function defined by a function template, the compiler is able to generate the function from the types of the arguments used. The type parameter for the function template is implicitly defined by the specific use of a particular function. Class templates are a little different. To create an object based on a class template, you must always specify the type parameter following the class name in the declaration. For example, to declare a CSamples<> object to handle samples of type double, you could write the declaration as: CSamples<double> myData(10.0);
This defines an object of type CSamples<double> that can store samples of type double, and the object is created with one sample stored with the value 10.0.
Try It Out
Class Templating
You could create an object from the CSamples<> template that stores CBox objects. This works because the CBox class implements the operator>() function to overload the greater-than operator. You could exercise the class template with the main() function in the following listing: // Ex8_07.cpp // Using a class template #include
431
Chapter 8 using std::cout; using std::endl; // Put the CBox class definition from Ex8_06.cpp here... // CSamples class template definition template class CSamples { public: // Constructors CSamples(const T values[], int count); CSamples(const T& value); CSamples(){ m_Free = 0; } bool Add(const T& value); T Max() const; private: T m_Values[100]; int m_Free;
// Insert a value // Calculate maximum
// Array to store samples // Index of free location in m_Values
}; // Constructor template definition to accept an template CSamples::CSamples(const T { m_Free = count < 100? count:100; // Don’t for(int i = 0; i < m_Free; i++) m_Values[i] = values[i]; // Store }
array of samples values[], int count) exceed the array count number of samples
// Constructor to accept a single sample template CSamples::CSamples(const T& value) { m_Values[0] = value; // Store the sample m_Free = 1; // Next is free } // Function to add a sample template bool CSamples::Add(const T& value) { bool OK = m_Free < 100; // Indicates there is a free place if(OK) m_Values[m_Free++] = value; // OK true, so store the value return OK; } // Function to obtain maximum sample template T CSamples::Max() { T theMax = m_Free ? m_Values[0] : 0; for(int i = 1; i < m_Free; i++) if(m_Values[i] > theMax) theMax = m_Values[i]; return theMax;
432
const // Set first sample or 0 as maximum // Check all the samples // Store any larger sample
More on Classes } int main() { CBox boxes[] = { CBox(8.0, 5.0, 2.0), CBox(5.0, 4.0, 6.0), CBox(4.0, 3.0, 3.0)
// Create an array of boxes // Initialize the boxes...
}; // Create the CSamples object to hold CBox objects CSamples myBoxes(boxes, sizeof boxes / sizeof CBox); CBox maxBox = myBoxes.Max(); // Get the biggest box cout << endl // and output its volume << “The biggest box has a volume of “ << maxBox.Volume() << endl; return 0; }
You should replace the comment with the CBox class definition from Ex8_06.cpp earlier in the chapter. You don’t need to worry about the operator>() function that supports comparison of a CBox object with a value of type double because this example does not need it. With the exception of the default constructor, all the member functions of the template are defined by separate function templates, just to show you a complete example of how it’s done. In main() you create an array of three CBox objects and then use this array to initialize a CSamples object that can store CBox objects. The declaration of the CSamples object is basically the same as it would be for an ordinary class, but with the addition of the type parameter in angled brackets following the template class name. The program generates the following output: The biggest box has a volume of 120
Note that when you create an instance of a class template, it does not follow that instance of the function templates for function members will also be created. The compiler only creates instances of templates for member functions that you actually call in your program. In fact, your function templates can even contain coding errors, and as long as you don’t call the member function that the template generates, the compiler does not complain. You can test this out with the example. Try introducing a few errors into the template for the Add() member. The program still compiles and run because it doesn’t call the Add() function. You could try modifying the example and perhaps seeing what happens when you instantiate classes by using the template with various other types. You might be surprised at what happens if you add some output statements to the class constructors. The constructor for the CBox is being called 103 times! Look at what is happening in the main() function. First you create an array of 3 CBox objects, so that’s 3 calls. You then create a CSamples object to hold them, but a CSamples object contains an array of 100 variables of type CBox, so you call the default constructor another 100 times, once for each element in the array. Of course, the maxBox object will be created by the default copy constructor that is supplied by the compiler.
433
Chapter 8
Class Templates with Multiple Parameters Using multiple type parameters in a class template is a straightforward extension of the example using a single parameter that you have just seen. You can use each of the type parameters wherever you want in the template definition. For example, you could define a class template with two type parameters: template class CExampleClass { // Class data members private: T1 m_Value1; T2 m_Value2; // Rest of the template definition... };
The types of the two class data members shown are determined by the types you supply for the parameters when you instantiate an object. The parameters in a class template aren’t limited to types. You can also use parameters that require constants or constant expressions to be substituted in the class definition. In our CSamples template, we arbitrarily defined the m_Values array with 100 elements. You could, however, let the user of the template choose the size of the array when the object is instantiated, by defining the template as: template class CSamples { private: T m_Values[Size]; // Array to store samples int m_Free; // Index of free location in m_Values public: // Constructor definition to accept an array of samples CSamples(const T values[], int count) { m_Free = count < Size? count:Size; // Don’t exceed the array for(int i = 0; i < m_Free; i++) m_Values[i] = values[i];
// Store count number of samples
} // Constructor to accept a single sample CSamples(const T& value) { m_Values[0] = value; // Store the sample m_Free = 1; // Next is free } // Default constructor CSamples() { m_Free = 0;
434
// Nothing stored, so first is free
More on Classes } // Function to add a sample int Add(const T& value) { int OK = m_Free < Size; if(OK) m_Values[m_Free++] = value; return OK; }
// Indicates there is a free place // OK true, so store the value
// Function to obtain maximum sample T Max() const { // Set first sample or 0 as maximum T theMax = m_Free ? m_Values[0] : 0; for(int i = 1; i < m_Free; i++) if(m_Values[i] > theMax) theMax = m_Values[i]; return theMax;
// Check all the samples // Store any larger sample
} };
The value supplied for Size when you create an object replaces the appearance of the parameter throughout the template definition. Now you can declare the CSamples object from the previous example as: CSamples MyBoxes(boxes, sizeof boxes/sizeof CBox);
Because you can supply any constant expression for the Size parameter, you could also have written this as: CSamples MyBoxes(boxes, sizeof boxes/sizeof CBox);
The example is a poor use of a template though(the original version was much more usable. A consequence of making Size a template parameter is that instances of the template that store the same types of objects but have different size parameter values are totally different classes and cannot be mixed. For instance, an object of type CSamples<double, 10> cannot be used in an expression with an object of type CSamples<double, 20>. You need to be careful with expressions that involve comparison operators when instantiating templates. Look at this statement: CSamples y ? 10 : 20 > MyType();
// Wrong!
This does not compile correctly because the > preceding y in the expression is interpreted as a rightangled bracket. Instead, you should write this statement as: CSamples y ? 10 : 20) > MyType();
// OK
435
Chapter 8 The parentheses ensure that the expression for the second template argument doesn’t get mixed up with the angled brackets.
Using Classes I’ve touched on most of the basic aspects of defining a native C++ class, so maybe we should look at how a class might be used to solve a problem. I’ll need to keep the problem simple to keep this book down to a reasonable number of pages, so we’ll consider problems in which we can use an extended version of the CBox class.
The Idea of a Class Interface The implementation of an extended CBox class should incorporate the notion of a class interface. We are going to provide a tool kit for anyone wanting to work with CBox objects so we need to assemble a set of functions that represents the interface to the world of boxes. Because the interface represents the only way to deal with CBox objects, it needs to be defined to cover adequately the likely things one would want to do with a CBox object, and be implemented, as far as possible, in a manner that protects against misuse or accidental errors. The first question that you need to consider in designing a class is the nature of the problem you intend to solve and, from that, determine the kind of functionality you need to provide in the class interface.
Defining the Problem The principal function of a box is to contain objects of one kind or another, so, in a word, the problem is packaging. We’ll attempt to provide a class that eases packaging problems in general and then see how it might be used. We assume that we’ll always be working on packing CBox objects into other CBox objects because, if you want to pack candy in a box, you can always represent each of the pieces of candy as an idealized CBox object. The basic operations that you might want to provide in the CBox class include:
436
❑
Calculate the volume of a CBox. This is a fundamental characteristic of a CBox object and you have an implementation of this already.
❑
Compare the volumes of two CBox objects to determine which is the larger. You probably should support a complete set of comparison operators for CBox objects. You already have a version of the > operator.
❑
Compare the volume of a CBox object with a specified value and vice versa. You also have an implementation of this for the > operator, but you will also need to implement functions supporting the other comparison operators.
❑
Add two CBox objects to produce a CBox object, which will contain both the original objects. Thus, the result will be at least the sum of the volumes, but may be larger. You have a version of this already that overloads the + operator.
❑
Multiply a CBox object by an integer (and vice versa) to provide a CBox object, which will contain a specified number of the original objects. This is effectively designing a carton.
More on Classes ❑
Determine how many CBox objects of a given size can be packed in another CBox object of a given size. This is effectively division, so you could implement this by overloading the / operator.
❑
Determine the volume of space remaining in a CBox object after packing it with the maximum number of CBox objects of a given size.
I had better stop right there! There are undoubtedly other functions that would be very useful but, in the interest of saving trees, we’ll consider the set to be complete, apart from ancillaries such as accessing dimensions, for example.
Implementing the CBox Class You really need to consider the degree of error protection that you want to build into the CBox class. The basic class that you defined to illustrate various aspects of classes is a starting point, but you should also consider some points a little more deeply. The constructor is a little weak in that it doesn’t ensure that the dimensions for a CBox are valid so perhaps the first thing you should do is to ensure that you always have valid objects. You could redefine the basic class as follows to do this: class CBox // Class definition at global scope { public: // Constructor definition CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) { lv = lv <= 0? 1.0: lv; // Ensure positive wv = wv <= 0? 1.0: wv; // dimensions for hv = hv <= 0? 1.0: hv; // the object m_Length = lv > wv? lv: wv; m_Width = wv < lv? wv: lv; m_Height = hv;
// Ensure that // length >= width
} // Function to calculate the volume of a box double Volume() const { return m_Length*m_Width*m_Height; } // Function providing the length of a box double GetLength() const { return m_Length; } // Function providing the width of a box double GetWidth() const { return m_Width; } // Function providing the height of a box double GetHeight() const { return m_Height; } private: double m_Length; double m_Width; double m_Height;
// Length of a box in inches // Width of a box in inches // Height of a box in inches
};
437
Chapter 8 The constructor is now secure because any dimension that the user of the class tries to set to a negative number or zero is set to 1 in the constructor. You might also consider displaying a message for a negative or zero dimension because there’s obviously an error when this occurs, and arbitrarily and silently setting a dimension to 1 might not be the best solution. The default copy constructor is satisfactory for our class because you have no dynamic memory allocation for data members, and the default assignment operator will also work as you would like. The default destructor also works perfectly well in this case so you don’t need to define it. Perhaps now you should consider what is required to support comparisons of objects of our class.
Comparing CBox Objects You should include support for the operators >, >=, ==, < and <= so that they work with both operands as CBox objects, as well between a CBox object and a value of type double. You can implement these as ordinary global functions because they don’t need to be member functions. You can write the functions that compare the volumes of two CBox objects in terms of the functions that compare the volume of a CBox object with a double value, so let’s start with the latter. You can start by repeating the operator>() function that you had before: // Function for testing if a constant is > a CBox object int operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); }
You can now write the operator<() function in a similar way: // Function for testing if a constant is < CBox object int operator<(const double& value, const CBox& aBox) { return value < aBox.Volume(); }
You can code the implementations of the same operators with the arguments reversed in terms of the two functions you have just defined: // Function for testing if CBox object is > a constant int operator>(const CBox& aBox, const double& value) { return value < aBox; } // Function for testing if CBox object is < a constant int operator<(const CBox& aBox, const double& value) { return value > aBox; }
You just use the appropriate overloaded operator function that you wrote before, with the arguments from the call to the new function switched.
438
More on Classes The functions implementing the >= and <= operators are the same as the first two functions, but with the <= operator replacing each use of <, and >= instead of >; there’s little point in reproducing them at this stage. The operator==() functions are also very similar: // Function for testing if constant is == the volume of a CBox object int operator==(const double& value, const CBox& aBox) { return value == aBox.Volume(); } // Function for testing if CBox object is == a constant int operator==(const CBox& aBox, const double& value) { return value == aBox; }
You now have a complete set of comparison operators for CBox objects. Keep in mind that these also work with expressions as long as the expressions result in objects of the required type, so you are able to combine them with the use of other overloaded operators.
Combining CBox Objects Now you come to the question of overloading the operators +, *, /, and %. I will take them in order. The add operation that you already have from Ex8_06.cpp has this prototype: CBox operator+(const CBox& aBox);
// Function adding two CBox objects
Although the original implementation of this isn’t an ideal solution, let’s use it anyway to avoid overcomplicating the class. A better version would need to examine whether the operands had any faces with the same dimensions and if so, join along those faces, but coding that could get a bit messy. Of course, if this were a practical application, a better add operation could be developed later and substituted for the existing version, and any programs written using the original would still run without change. The separation of the interface to a class from its implementation is crucial to good C++ programming. Notice that I conveniently forgot the subtraction operator. This is a judicious oversight to avoid the complications inherent in implementing this. If you’re really enthusiastic about it, and you think it’s a sensible idea, you can give it a try — but you need to decide what to do when the result has a negative volume. If you allow the concept, you need to resolve which box dimension or dimensions are to be negative, and how such a box is to be handled in subsequent operations. The multiply operation is very easy. It represents the process of creating a box to contain n boxes, where n is the multiplier. The simplest solution would be to take the m_Length and m_Width of the object to be packed and multiply the height by n to get the new CBox object. You can make it a little more clever by checking whether or not the multiplier is even and, if it is, stack the boxes side-by-side by doubling the m_Width value and only multiplying the m_Height value by half of n. This mechanism is illustrated in Figure 8-6.
439
Chapter 8 L
CBox Multiply: n odd : 3*aBox
W
3*H
L
W
H
L
CBox Multiply: n even : 6*aBox
2*W
3*H
Figure 8-6
Of course, you don’t need to check which is the larger of the length and width for the new object because the constructor will sort it out automatically. You can write the version of the operator*() function as a member function with the left operand as a CBox object: // CBox multiply operator this*n CBox operator*(int n) const { if(n % 2) return CBox(m_Length, m_Width, n*m_Height);
440
// n odd
More on Classes else return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height);
// n even
}
Here, you use the % operator to determine whether n is even or odd. If n is odd, the value of n % 2 is 1 and the if statement is true. If it’s even, n % 2 is 0 and the statement is false. You can now use the function you have just written in the implementation of the version with the left operand as an integer. You can write this as an ordinary non-member function: // CBox multiply operator n*aBox CBox operator*(int n, const CBox& aBox) { return aBox*n; }
This version of the multiply operation simply reverses the order of the operands so as to use the previous version of the function directly. That completes the set of arithmetic operators for CBox objects that you defined. We can finally look at the two analytical operator functions, operator/() and operator%().
Analyzing CBox Objects As I have said, the division operation determines how many CBox objects are identical to that specified by the right operand can be contained in the CBox object specified by the left operand. To keep it relatively simple, assume that all the CBox objects are packed the right way up, that is, with the height dimensions vertical. Also assume that they are all packed the same way round, so that their length dimensions are aligned. Without these assumptions, it can get rather complicated. The problem then amounts to determining how many of the right-operand objects can be placed in a single layer, and then deciding how many layers we can get inside the left-operand CBox. You can code this as a member function like this: int operator/(const CBox& aBox) { int tc1 = 0; // Temporary for number in horizontal plane this way int tc2 = 0; // Temporary for number in a plane that way tc1 = static_cast((m_Length static_cast((m_Width tc2 = static_cast((m_Length static_cast((m_Width
/ / / /
aBox.m_Length))* aBox.m_Width)); // to fit this way aBox.m_Width))* aBox.m_Length)); // and that way
//Return best fit return static_cast((m_Height/aBox.m_Height)*(tc1>tc2 ? tc1 : tc2)); }
This function first determines how many of the right-operand CBox objects can fit in a layer with their lengths aligned with the length dimension of the left-operand CBox. This is stored in tc1. You then calculate how many can fit in a layer with the lengths of the right-operand CBoxes lying in the width direction of the left-operand CBox. Finally you multiply the larger of tc1 and tc2 by the number of layers you can pack in, and return that value. This process is illustrated in Figure 8-7.
441
Chapter 8
bBox
W=2 aBox
W=6
H=1
W=2
8/3 = 2
bBox bBox
2/1 = 2
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
6/2 = 3
bBox
8/2 = 4
6/2 = 3
2/1 = 2
With this configuration 12 can be stored Result is 16 Figure 8-7
We look at two possibilities: fitting bBox into aBox with the length aligned with that of aBox and then with the length of bBox aligned with the width of aBox. You can see from Figure 8-7 that the best packing results from rotating bBox so that the width divides into the length of aBox. The other analytical operator function, operator%(), for obtaining the free volume in a packed aBox is easier because you can use the operator you have just written to implement it. You can write it as an ordinary global function because you don’t need access to the private members of the class. // Operator to return the free volume in a packed box double operator%(const CBox& aBox, const CBox& bBox) { return aBox.Volume() - ((aBox/bBox)*bBox.Volume()); }
This computation falls out very easily using existing class functions. The result is the volume of the big box, aBox, minus the volume of the bBox boxes that can be stored in it. The number of bBox objects packed into aBox is given by the expression aBox/bBox, which uses the previous overloaded operator. You multiply this by the volume of bBox objects to get the volume to be subtracted from the volume of the large box, aBox.
442
More on Classes That completes the class interface. Clearly, there are many more functions that might be required for a production problem solver, but, as an interesting working model demonstrating how you can produce a class for solving a particular kind of problem, it will suffice. Now you can go ahead and try it out on a real problem.
Try It Out
A Multifile Project Using the CBox Class
Before you can actually start writing the code to use the CBox class and its overloaded operators, first you need to assemble the definition for the class into a coherent whole. You’re going to take a rather different approach from what you’ve seen previously, in that you’re going to write multiple files for the project. You’re also going to start using the facilities that Visual C++ 2005 provides for creating and maintaining code for our classes. This means that you do less of the work, but it will also mean that the code is slightly different in places. Start by creating a new WIN32 project for a console application called Ex8_08 and check the Empty project application option. If you select the Class View tab, you’ll see the window shown in Figure 8-8.
Figure 8-8
This shows a view of all the classes in a project, but of course, there are none here for the moment. Although there are no classes defined — or anything else for that matter — Visual C++ 2005 has already made provision for including some. You can use Visual C++ 2005 to create a skeleton for our CBox class, and the files that relate to it too. Right-click Ex8_08 in Class View and select Add/Class from the popup menu that appears. You can then select C++ from the class categories in the left pane of the Add
443
Chapter 8 Class dialog box that displays and the C++ Class template in the right pane and press Enter. You can then enter the name of the class that you want to create, CBox, in the Generic Class Wizard dialog box as
shown in Figure 8-9.
Figure 8-9
The name of the file that’s indicated on the dialog, Box.cpp, is used to contain the class implementation, which consists of the definitions for the function members of the class. This is the executable code for the class. You can change the name of this file if you want, but Box.cpp looks like a good name for the file in this case. The class definition will be stored in a file called Box.h. This is the standard way of structuring a program. Code that consists of class definitions is stored in files with the extension .h, and code that defines functions is stored in files with the extension .cpp. Usually, each class definition goes in its own .h file, and each class implementation goes in its own .cpp file. When you click on the Finish button in the dialog box, two things happen:
1.
A file Box.h is created, containing a skeleton definition for the class CBox. This includes a noargument constructor and a destructor.
2.
A file Box.cpp is created containing a skeleton implementation for the functions in the class with definitions for the constructor and the destructor(both bodies are empty, of course.
The editor pane displaying the code should be as shown in Figure 8-10. If it is not presently displayed, just double-click CBox in Class View, and it should appear.
444
More on Classes
Figure 8-10
As you can see, there are two controls above the pane containing the code listing for the class. The left control displays the current class name, CBox, and clicking the button to the right of the class name displays the list of all the classes in the project. In general you can use this control to switch to another class by selecting it from the list, but here you have just one class defined. The control to the right relates to the members defined in the .cpp file for the current class, and clicking its button displays the members of the class. Selecting a member from the list causes its code to be visible in the pane below. Let’s start developing the CBox class based on what Visual C++ has provided automatically for us.
Defining the CBox Class If you click on the + to the left of Ex8_08 in the Class View, the tree expands and you see that CBox is now defined for the project. All the classes in a project are displayed in this tree. You can view the source code supplied for the definition of a class by double-clicking the class name in the tree, or by using the controls above the pane displaying the code as I described in the previous section. The CBox class definition that was generated starts with a preprocessor directive: #pragma once
The effect of this is to prevent file from being opened and included into the source code more than once by the compiler in a build. Typically a class definition is included into several files in a project because each file that references the name of a particular class needs access to its definition. In some instances, a header file may itself have #include directives for other header files. This can result in the possibility of the contents of a header file appearing more than once in the source code. Having more than one definition of a class in a build is not allowed and will be flagged as an error. Having the #pragma once directive at the start of every header file ensures this cannot happen. Note that #pragma once is a Microsoft-specific directive that may not be supported in other development environments. If you are developing code that you anticipate may need to be compiled in other environments, you can use the following form of directive in a header file to achieve the same effect:
445
Chapter 8 // Box.h header file #ifndef BOX_H #define BOX_H // Code that must not be included more than once // such as the CBox class definition #endif
The important lines are shaded and correspond to directives that supported by any ISO/ANSI C++ compiler. The lines following the #ifndef directive down to the #endif directive are included in a build as long as the symbol BOX_H is not defined. The line following #ifndef defines the symbol BOX_H thus ensuring that the code in this header file are be included a second time. Thus this has the same effect as placing the #pragma once directive at the beginning of a header file. Clearly the #pragma once directive is simpler and less cluttered so it’s better to use that when you only expect to be using your code in the Visual C++ 2005 development environment. You sometimes see the #ifndef/#endif combination written as: #if !defined BOX_H #define BOX_H // Code that must not be included more than once // such as the CBox class definition #endif
The Box.cpp file that was generated by Class wizard contains the following code: #include “Box.h” CBox::CBox(void) { } CBox::~CBox(void) { }
The first line is an #include preprocessor directive that has the effect of including the contents of the Box.h file(the class definition(into this file, Box.cpp. This is necessary because the code in Box.cpp refers to the CBox class name and the class definition needs to be available to assign meaning to the name CBox.
Adding Data Members You can add the private data members m_Length, m_Width, and m_Height. Right-click CBox in Class View and select Add/Add Variable from the context menu. You can then specify the name, type, and access for first data member that you want to add to the class in the Add Member Variable Wizard dialog box. The way you specify a new data member in this dialog box is quite explanatory. If you specify a lower limit for a data member, you must also specify an upper limit. When you specify limits, the constructor definition in the .cpp file will be modified to add a default value for the data member corresponding to the lower limit. You can add a comment in the lower input field if you wish. When you click on the OK button, the variable is added to the class definition along with the comment if you have supplied one. You should repeat the process for the other two class data members, m_Width and m_Height. The class definition in Box.h is then modified to look like this:
446
More on Classes #pragma once class CBox { public: CBox(void); public: ~CBox(void); private: // Length of a box in inches double m_Length; // Width of a box in inches double m_Width; // Height of a box in inches double m_Height; };
Of course, you’re quite free to enter the declarations for these members manually, directly into the code, if you want. You always have the choice of whether you use the automation provided by the IDE. You can also manually delete anything that was generated automatically, but don’t forget that sometimes both the .h and .cpp file need to be changed. It’s a good idea to save all the files whenever you make manual changes as this will cause the information in Class View to be updated. If you look in the Box.cpp file, you’ll see that the wizard has also added an initialization list to the constructor definition for the data members you have added, with each variable initialized to 0. You’ll modify the constructor to do what you want next.
Defining the Constructor You need to change the declaration of the no-arg constructor in the class definition so that it has arguments with default values, so modify it to: CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0);
Now you’re ready to implement it. Open the Box.cpp file if it isn’t open already and modify the constructor definition to: CBox::CBox(double lv, double wv, double hv) { lv = lv <= 0.0 ? 1.0 : lv; // Ensure positive wv = wv <= 0.0 ? 1.0 : wv; // dimensions for hv = hv <= 0.0 ? 1.0 : hv; // the object m_Length = lv>wv ? lv : wv; m_Width = wv
// Ensure that // length >= width
}
Remember that the initializers for the parameters to a member function should only appear in the member declaration in the class definition, not in the definition of the function. If you put them in the function
447
Chapter 8 definition, your code will not compile. You’ve seen this code already, so I won’t discuss it again. It would be a good idea to save the file at this point by clicking the Save toolbar button. Get into the habit of saving the file you’re editing before you switch to something else. If you need to edit the constructor again, you can get to it easily by either double-clicking its entry in the lower pane on the Class View tab or by selecting it from the right drop-down menu above the pane displaying the code. You can also get to a member function’s definition in a .cpp file or to its declaration in a .h file directly by right-clicking its name in the Class View pane and selecting the appropriate item from the context menu that appears.
Adding Function Members You need to add all the functions you saw earlier to the CBox class. Previously, you defined several function members within the class definition, so that these functions were automatically inline. You can achieve the same result by entering the code in the class definition for these functions manually, or you can use the Add Member Function wizard. You might think that you can define each inline function in the .cpp file, and add the keyword inline to the function definitions, but the problem here is that inline functions end up not being “‘real” functions. Because the code from the body of each function has to be inserted directly at the position it is called, the definitions of the functions need to be available when the file containing calls to the functions is compiled. If they’re not, you’ll get linker errors and your program will not run. If you want member functions to be inline, you must include the function definitions in the .h file for the class. They can be defined either within the class definition, or immediately following it in the .h file. You should put any global inline functions you need into a .h file and #include that file into any .cpp file that uses them. To add the GetHeight() function as inline, right-click on CBox on the Class View tab and select Add > Add Function from the context menu. You then can enter the data defining the function in the dialog that is displayed, as Figure 8-11 shows.
Figure 8-11
448
More on Classes You can specify the return type to be double by selecting from the drop-down list, but you could equally well type it in. Obviously, for a type that does not appear in the list, you would just enter it from the keyboard. Selecting the inline checkbox ensures that GetHeight() will be created as an inline function. Note the other options to declare a function as static, virtual, or pure. As you know, a static member function exists independently of any objects of a class. We’ll get to virtual and pure virtual functions in Chapter 9. The GetHeight() function has no parameters so nothing further needs to be added. Clicking the OK button will add the function definition to the class definition in Box.h. If you repeat this process for the GetWidth(), GetLength(), and Volume() member functions, the CBox class definition in Box.h will look like this: #pragma once class CBox { public: CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0); ~CBox(void); private: // Length of a box in inches double m_Length; // Width of a box in inches double m_Width; // Height of a box in inches double m_Height; public: double GetHeight(void) { return 0; } double GetWidth(void) { return 0; } double CBox::GetLength(void) { return 0; } // Calculate the volume of a box double Volume(void) { return 0; } };
An additional public section has been added to the class definition that contains the inline function definitions. You need to modify each of the definitions to provide the correct return value and to declare the functions to be const. For example, the code for GetHeight() function should be changed to: double GetHeight(void) const { return m_Height; }
449
Chapter 8 You can change the definitions of the GetWidth() and GetLength() functions in a similar way. The Volume() function definition should be changed to: double Volume(void) const { return m_Length*m_Width*m_Height; }
You could enter the other non-inline member functions directly in the editor pane that shows the code, but of course you can also use the Add Member Function wizard to do it, and the practice will be useful. Right-click CBox in the Class View tab and select the Add/Add Function menu item from the context menu, as before. You can then enter the details of the first function you want to add in the dialog that appears, as Figure 8-12 shows.
Figure 8-12
Here I have defined the operator+() function as public with a return type of CBox. The parameter type and name have also been entered in the appropriate fields. You must click the Add button to register the parameter as being in the parameter list before clicking the Finish button. This also updates the function signature shown at the bottom of the Add Member Function Wizard dialog box. You could then enter details of another parameter if there was more than one and click Add once again to add it. I have also entered a comment in the dialog box and the wizard will insert this in both Box.h and Box.cpp. When you click on Finish, the declaration for the function is added to the class definition in the Box.h file, and a skeleton definition for the function is added to the Box.cpp file. The function needs to be declared as const so you must add this keyword to the declaration of the operator+() function within the class definition, and to the definition of the function on Box.cpp. You must also add the code in the body of the function, like this:
450
More on Classes CBox CBox::operator +(const CBox& aBox) const { // New object has larger length and width of the two, // and sum of the two heights return CBox(m_Length > aBox.m_Length ? m_Length : aBox.m_Length, m_Width > aBox.m_Width ? m_Width : aBox.m_Width, m_Height + aBox.m_Height); }
You need to repeat this process for the operator*() and operator/() functions that you saw earlier. When you have completed this, the class definition in Box.h looks something like this: #pragma once class CBox { public: CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0); ~CBox(void); private: // Length of a box in inches double m_Length; // Width of a box in inches double m_Width; // Height of a box in inches double m_Height; public: double GetHeight(void) const { return m_Height; } public: double GetWidth(void) const { return m_Width; } public: double GetLength(void) const { return m_Length; } public: double Volume(void) const { return m_Length*m_Width*m_Height; }
451
Chapter 8 public: // Overloaded addition operator CBox operator+(const CBox& aBox) const; public: // Multiply a box by an integer CBox operator*(int n) const; public: // Divide one box into another int operator/(const CBox& aBox) const; };
You can edit or rearrange the code in any way that you want(as long as it’s still correct, of course. I have added a few empty lines to make the code a bit more readable. The contents of the Box.cpp file ultimately look something like this: #include “.\box.h” CBox::CBox(double lv, double wv, double hv) { lv = lv <= 0.0 ? 1.0 : lv; // Ensure positive wv = wv <= 0.0 ? 1.0 : wv; // dimensions for hv = hv <= 0.0 ? 1.0 : hv; // the object m_Length = lv>wv ? lv : wv; m_Width = wv
// Ensure that // length >= width
} CBox::~CBox(void) { } // Overloaded addition operator CBox CBox::operator+(const CBox& aBox) const { // New object has larger length and width of the two, // and sum of the two heights return CBox(m_Length > aBox.m_Length ? m_Length : aBox.m_Length, m_Width > aBox.m_Width ? m_Width : aBox.m_Width, m_Height + aBox.m_Height); } // Multiply a box by an integer CBox CBox::operator*(int n) const { if(n%2) return CBox(m_Length, m_Width, n*m_Height); else return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height);
452
// n odd // n even
More on Classes } // Divide one box into another int CBox::operator/(const CBox& aBox) const { // Temporary for number in horizontal plane this way int tc1 = 0; // Temporary for number in a plane that way int tc2 = 0; tc1 = static_cast((m_Length/aBox.m_Length))* static_cast((m_Width/aBox.m_Width));
// to fit this way
tc2 = static_cast((m_Length/aBox.m_Width))* static_cast((m_Width/aBox.m_Length));
// and that way
//Return best fit return static_cast((m_Height/aBox.m_Height))*(tc1>tc2 ? tc1 : tc2); }
The shaded lines are those that you should have modified or added manually. The very short functions, particularly those that just return the value of a data member, have their definitions within the class definition so that they are inline. If you take a look at Class View by clicking the tab and then click the + beside the CBox class name, you’ll see that all the members of the class are shown in the lower pane. This completes the CBox class, but you still need to define the global functions that implement operators to compare the volume of a CBox object with a numerical value.
Adding Global Functions You need to create a .cpp file that will contain the definitions for the global functions supporting operations on CBox objects. The file also needs to be part of the project. Click the Solution Explorer tab to display it (you currently will have the Class View tab displayed) and right-click the Source Files folder. Select Add > New Item from the context menu to display the dialog box. Choose the category as Code and the template as C++ File (.cpp) in the right pane of the dialog box and enter the file name as BoxOperators. You can now enter the following code in the Editor pane: // BoxOperators.cpp // CBox object operations that don’t need to access private members #include “Box.h” // Function for testing if a constant is > a CBox object bool operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); } // Function for testing if a constant is < CBox object bool operator<(const double& value, const CBox& aBox) { return value < aBox.Volume(); } // Function for testing if CBox object is > a constant
453
Chapter 8 bool operator>(const CBox& aBox, const double& value) { return value < aBox; } // Function for testing if CBox object is < a constant bool operator<( const CBox& aBox, const double& value) { return value > aBox; } // Function for testing if a constant is >= a CBox object bool operator>=(const double& value, const CBox& aBox) { return value >= aBox.Volume(); } // Function for testing if a constant is <= CBox object bool operator<=(const double& value, const CBox& aBox) { return value <= aBox.Volume(); } // Function for testing if CBox object is >= a constant bool operator>=( const CBox& aBox, const double& value) { return value <= aBox; } // Function for testing if CBox object is <= a constant bool operator<=( const CBox& aBox, const double& value) { return value >= aBox; } // Function for testing if a constant is == CBox object bool operator==(const double& value, const CBox& aBox) { return value == aBox.Volume(); } // Function for testing if CBox object is == a constant bool operator==(const CBox& aBox, const double& value) { return value == aBox; } // CBox multiply operator n*aBox CBox operator*(int n, const CBox& aBox) { return aBox * n; } // Operator to return the free volume in a packed CBox double operator%( const CBox& aBox, const CBox& bBox) { return aBox.Volume() - (aBox / bBox) * bBox.Volume(); }
You have a #include directive for Box.h because the functions refer to the CBox class. Save the file. When you have completed this, you can select the Class View tab. The Class View tab now includes a Global Functions and Variables folder that contain all the functions you just added. You have seen definitions for all these functions earlier in the chapter, so I won’t discuss their implementations again. When you want to use any of these functions in another .cpp file, you’ll need to be sure that you declare all the functions that you use so the compiler will recognize them. You can achieve this by putting a set of declarations in a header file. Switch back to the Solution Explorer pane once more and right-click the Header Files folder name. Select Add > New Item from the context menu to display the dialog box, but this time select category as Code, the template asHeader File(.h), and enter the name as BoxOperators. After clicking the Add button an empty header file is added to the project, and you can add the following code in the Editor window:
454
More on Classes // BoxOperators.h - Declarations for global box operators #pragma once bool operator>(const double& value, const CBox& aBox); bool operator<(const double& value, const CBox& aBox); bool operator>(const CBox& aBox, const double& value); bool operator<(const CBox& aBox, const double& value); bool operator>=(const double& value, const CBox& aBox); bool operator<=(const double& value, const CBox& aBox); bool operator>=(const CBox& aBox, const double& value); bool operator<=(const CBox& aBox, const double& value); bool operator==(const double& value, const CBox& aBox); bool operator==(const CBox& aBox, const double& value); CBox operator*(int n, const CBox aBox); double operator%(const CBox& aBox, const CBox& bBox); The #pragma once directive ensures that the contents of the file are not included more than once in a build. You just need to add an #include directive for BoxOperators.h to any source file that makes use of any of these functions.
You’re now ready to start applying these functions, along with the CBox class, to a specific problem in the world of boxes.
Using Our CBox Class Suppose that you are packaging candies. The candies are on the big side, real jaw breakers, occupying an envelope 1.5 inches long by 1 inch wide by 1 inch high. You have access to a standard candy box that is 4.5 inches by 7 inches by 2 inches, and you want to know how many candies will fit in the box so that you can set the price. You also have a standard carton that is 2 feet 6 inches long, by 18 inches wide and 18 inches deep, and you want to know how many boxes of candy it can hold and how much space you’re wasting when it has been filled. In case the standard candy box isn’t a good solution, you would also like to know what custom candy box would be suitable. You know that you can get a good price on boxes with a length from 3 inches to 7 inches, a width from 3 inches to 5 inches and a height from 1 inch to 2.5 inches, where each dimension can vary in steps of half an inch. You also know that you need to have at least 30 candies in a box, because this is the minimum quantity consumed by your largest customers at a sitting. Also, the candy box should not have empty space, because the complaints from customers who think they are being cheated goes up. Further, ideally you want to pack the standard carton completely so the candies don’t rattle around. You don’t want to be too stringent about this otherwise packing could become difficult, so let’s say you have no wasted space if the free space in the packed carton is less than the volume of a single candy box. With the CBox class, the problem becomes almost trivial; the solution is represented by the following main() function. Add a new C++ source file, Ex8_08.cpp, to the project through the context menu you get when you right-click Source Files in the Solution Explorer pane, as you’ve done before. You can then type in the code shown here:
455
Chapter 8 // Ex8_08.cpp // A sample packaging problem #include #include “Box.h” #include “BoxOperators.h” using std::cout; using std::endl; int main() { CBox candy(1.5, 1.0, 1.0); CBox candyBox(7.0, 4.5, 2.0); CBox carton(30.0, 18.0, 18.0);
// Candy definition // Candy box definition // Carton definition
// Calculate candies per candy box int numCandies = candyBox/candy; // Calculate candy boxes per carton int numCboxes = carton/candyBox; // Calculate wasted carton space double space = carton%candyBox; cout << << << << << << <<
endl “There are “ << numCandies “ candies per candy box” endl “For the standard boxes there are “ << numCboxes “ candy boxes per carton “ << endl << “with “ space << “ cubic inches wasted.”;
cout << endl << endl << “CUSTOM CANDY BOX ANALYSIS (No Waste)”; // Try the whole range of custom candy boxes for(double length = 3.0 ; length <= 7.5 ; length += 0.5) for(double width = 3.0 ; width <= 5.0 ; width += 0.5) for(double height = 1.0 ; height <= 2.5 ; height += 0.5) { // Create new box each cycle CBox tryBox(length, width, height); if(carton%tryBox < tryBox.Volume() && tryBox % candy == 0.0 && tryBox/candy >= 30) cout << endl << endl << “Trial Box L = “ << tryBox.GetLength() << “ W = “ << tryBox.GetWidth() << “ H = “ << tryBox.GetHeight() << endl << “Trial Box contains “ << tryBox / candy << “ candies” << “ and a carton contains “ << carton / tryBox << “ candy boxes.”; } cout << endl; return 0; }
456
More on Classes Let’s first look at how the program is structured. You have divided it into a number of files, which is common when writing in C++. You will be able to see them if you look at the Solution Explorer tab, which looks as shown in Figure 8-13.
Figure 8-13
The file Ex8_08.cpp contains the main()function and a #include directive for the file BoxOperators.h that contains the prototypes for the functions in BoxOperators.cpp (which aren’t class members). It also has an #include directive for the definition of the class CBox in Box.h. A C++ console program is usually divided into a number of files that will each fall into one of three basic categories:
1.
.h files containing library #include commands, global constants and variables, class defini-
tions and function prototypes — in other words, everything except executable code. They also contain inline function definitions. Where a program has several class definitions, they are often placed in separate .h files.
2.
.cpp files containing the executable code for the program, plus #include commands for all the definitions required by the executable code.
3.
Another .cpp file containing the function main().
The code in our main()function really doesn’t need a lot of explanation — it’s almost a direct expression of the definition of the problem in words, because the operators in the class interface perform problemoriented actions on CBox objects. The solution to the question of the use of standard boxes is in the declaration statements, which also compute the answers we require as initializing values. You then output these values with some explanatory comments.
457
Chapter 8 The second part of the problem is solved using the three nested for loops iterating over the possible ranges of m_Length, m_Width and m_Height so that you evaluate all possible combinations. You could output them all as well, but because this would involve 200 combinations, of which you might only be interested in a few, you have an if statement that identifies the options that you’re actually interested in. The if expression is only true if there’s no space wasted in the carton and the current trial candy box has no wasted space and it contains at least 30 candies. Here’s the output from this program: There are 42 candies per candy box For the standard boxes there are 144 candy boxes per carton with 648 cubic inches wasted. CUSTOM CANDY BOX ANALYSIS (No Waste) Trial Box L = 5 W = 4.5 H = 2 Trial Box contains 30 candies and a carton contains 216 candy boxes. Trial Box L = 5 W = 4.5 H = 2 Trial Box contains 30 candies and a carton contains 216 candy boxes. Trial Box L = 6 W = 4.5 H = 2 Trial Box contains 36 candies and a carton contains 180 candy boxes. Trial Box L = 6 W = 5 H = 2 Trial Box contains 40 candies and a carton contains 162 candy boxes. Trial Box L = 7.5 W = 3 H = 2 Trial Box contains 30 candies and a carton contains 216 candy boxes.
You have a duplicate solution due to the fact that, in the nested loop, you evaluate boxes that have a length of 5 and a width of 4.5, as well as boxes that have a length of 4.5 and a width of 5. Because the CBox class constructor ensures that the length is not less than the width, these two are identical. You could include some additional logic to avoid presenting duplicates, but it hardly seems worth the effort. You could treat it as a small exercise if you like.
Organizing Your Program Code In this last example, you distributed the code among several files for the first time. Not only is this common practice with C++ applications generally, but with Windows programming it is essential. The sheer volume of code involved in even the simplest program necessitates dividing it into workable chunks. As discussed in the previous section, there are basically two kinds of source code file in a C++ program, .h files and .cpp files. This is illustrated in Figure 8-14. There’s the executable code that corresponds to the definitions of the functions that make up the program. Also, there are definitions of various kinds that are necessary for the executable code to compile correctly. These are global constants and variables, data types that include classes, structures, and unions, and function prototypes. The executable source code is stored in files with the extension .cpp, and the definitions are stored in files with the extension .h.
458
More on Classes
Function Definitions
Function Definitions Function Definitions Definition of main()
Source files with the extension .cpp
Class Definition
Class Definition Global Constants Global Constants
Header files with the extension .h
Figure 8-14
From time to time, you might want to use code from existing files in a new project. In this case you only have to add the .cpp files to the project, which you can do by using the Project > Add Existing Item menu option, or by right-clicking either Source Files or Header Files in the Solution Explorer tab and selecting Add > Existing Item from the context menu to add the file to your project. You don’t need to add .h files to your project, although you can if you want them to be shown in the Solution Explorer pane immediately. The code from .h files is added at the beginning of the .cpp files that require them as a result of the #include directives that you specify. You need #include directives for header files containing standard library functions and other standard definitions, as well as for your own header files. Visual C++ 2005 automatically keeps track of all these files, and enables you to view them in the Solution Explorer tab. As you saw in the last example, you can also view the class definitions and global constants and variables in the Class View tab.
459
Chapter 8 In a Windows program, there are other kinds of definitions for the specification of things such as menus and toolbar buttons. These are stored in files with extensions like .rc and .ico. Just like .h files, these do not need to be explicitly added to a project because they are created and tracked automatically by Visual C++ 2005 when you need them.
Naming Program Files As I have already said, for classes of any complexity, it’s usual to store the class definition in a .h file with a filename based on the class name, and to store the implementation of the function members of the class that are defined outside the class definition in a .cpp file with the same name. On this basis, the definition of our CBox class appeared in a file with the name Box.h. Similarly, the class implementation was stored in the file Box.cpp. We didn’t follow this convention in the earlier examples in the chapter because the examples were very short, and it was easier to reference the examples with names derived from the chapter number and the sequence number of the example within the chapter. With programs of any size though it becomes essential to structure the code in this way, so it would be a good idea to get into the habit of creating .h and .cpp files to hold your program code from now on. Segmenting a C++ program into .h and .cpp files is a very convenient approach, as it makes it easy for you to find the definition or implementation of any class, particularly if you’re working in a development environment that doesn’t have all the tools that Visual C++ provides. As long as you know the class name, you can go directly to the file you want. This isn’t a rigid rule, however. It’s sometimes useful to group the definitions of a set of closely related classes together in a single file and assemble their implementations similarly; however you choose to structure your files, the Class View still displays all the individual classes, as well as all the members of each class, as you can see in Figure 8-15.
Figure 8-15
460
More on Classes I adjusted the size of the Class View pane so all the elements in the project are visible. Here, you can see the details of the classes and globals for the last example. As mentioned before, double-clicking any of the entries in the tree takes you directly to the relevant source code.
C++/CLI Programming Although you can define a destructor in a reference class in the same way as you do for native C++ classes, most of the time it is not necessary; however, I’ll return to the topic of destructors for reference classes in the next chapter. You can also call delete for a handle to a reference class, but again, this is not normally necessary as the garbage collector will delete unwanted objects automatically. C++/CLI classes support overloading of operators but there are some differences that you need to explore. First of all, consider some basic differences between operator overloading C++/CLI classes and in native C++ classes. A couple of differences you have already heard about. You’ll probably recall that you must not overload the assignment operator in your value classes because the process for the assignment of one value class object to another of the same type is already defined to be member-by-member copying and you cannot change this. I also mentioned that unlike native classes, a ref class does not have a default assignment operator(if you want the assignment operator to work with your ref class objects then you must implement the appropriate function. Another difference from native C++ classes is that functions that implement operator overloading in C++/CLI classes can be static members of a class as well as instance members. This means that you have the option of implementing binary operators in C++/CLI classes with static member functions with two parameters in addition to the possibilities you have seen in the context of native C++ for operator functions as instance functions with one parameter or non-member functions with two parameters. Similarly, in C++/CLI you have the additional possibility to implement a prefix unary operator as a static member function with no parameters. Finally, although in native C++ you can overload the new operator, you cannot overload the gcnew operator in a C++/CLI class. Let’s look into some of the specifics, starting with value classes.
Overloading Operators in Value Classes Define a class to represent a length in feet and inches and use that as a base for demonstrating how operator overloading can be implemented for a value class. Addition seems like a good place to start, so here’s the Length value class, complete with the addition operator function: value class Length { private: int feet; int inches;
// Feet component // Inches component
public: static initonly int inchesPerFoot = 12; // Constructor Length(int ft, int ins) : feet(ft), inches(ins){ } // A length as a string
461
Chapter 8 virtual String^ ToString() override { return feet+L” feet “ + inches + L” inches”;
}
// Addition operator Length operator+(Length len) { int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet); return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); } };
The constant, inchesPerFoot is static so it is directly available to static and non-static function members of the class. Declaring inchesPerFoot as initonly means that it cannot be modified so it can be a public member of the class. There’s a ToString() function override defined for the class so you can write Length objects to the command line using the Console::WriteLine() function. The operator+() function implementation is very simple. The function returns a new Length object produced by combining the feet and inches component for the current object and the parameter, len. The calculation is done by combining the two lengths in inches and then computing the arguments to the Length class constructor for the new object from the value for the combined lengths in inches. The following code fragment would exercise the new operator function for addition. Length len1 = Length(6, 9); Length len2 = Length(7, 8); Console::WriteLine(L”{0} plus {1} is {2}”, len1, len2, len1+len2);
The last argument to the WriteLine() function is the sum of two Length objects so this invokes the operator+() function. The result is a new Length object for which the compiler arranges to call the ToString() function so the last statement is really the following: Console::WriteLine(L”{0} plus {1} is {2}”, len1, len2, len1.operator+(len2).ToString());
The execution of the code fragment results in the following output: 6 feet 9 inches plus 7 feet 8 inches is 14 feet 5 inches
Of course, you could define the operator+() function as a static member of the Length class, like this: static Length operator+(Length len1, Length len2) { int inchTotal = len1.inches+len2.inches+inchesPerFoot*(len1.feet+len2.feet); return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); } }
The parameters are the two Length objects to be added together to produce a new Length object. Because this is a static member of the class, the operator+() function is fully entitled to access the private members, feet and inches, of both the Length objects passed as arguments. Friend functions are not allowed in C++/CLI classes and an external function would not have access to private members of the class so you have no other possibilities for implementing the addition operator.
462
More on Classes Because we are not working with areas multiplication for Length objects really only makes sense multiplying a length by a numerical value. You can implement this as a static member of the class, but let’s define the function outside the class. The class looks like this: value class Length { private: int feet; int inches; public: static initonly int inchesPerFoot = 12; // Constructor Length(int ft, int ins) : feet(ft), inches(ins){ } // A length as a string virtual String^ ToString() override { return feet+L” feet “ + inches + L” inches”;
}
// Addition operator Length operator+(Length len) { int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet); return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); } static Length operator*(double x, Length len); // Pre-multiply by a double value static Length operator*(Length len, double x); // Post-multiply by a double value };
The new function declarations in the class provide for overloaded * operator functions to pre- and postmultiply a Length object by a value of type double. The definition of the operator*() function outside the class for pre-multiplication is: Length Length::operator *(double x, Length len) { int ins = safe_cast(x*len.inches +x*len.feet*inchesPerFoot); return Length(ins/12, ins %12); }
The post-multiplication version can now be implemented in terms of this: Length Length::operator *(Length len, double x) { return operator*(x, len); }
This just call the pre-multiply version with the arguments reversed. You could exercise these functions with the following fragment: double factor = 2.5; Console::WriteLine(L”{0} times {1} is {2}”, factor, len2, factor*len2); Console::WriteLine(L”{1} times {0} is {2}”, factor, len2, len2*factor);
463
Chapter 8 Both lines of output from this code fragment should reflect the same result from multiplication(19 feet 2 inches. The argument expression factor*len2 is equivalent to: Length::operator*(factor, len2).ToString()
The result of calling the static operator*() function is a new Length object and the ToString() function for that is called to produce the argument to the WriteLine() function. The expression len2*factor is similar but calls the operator*() function that has the parameters reversed. Although the operator*() functions have been written to deal with a multiplier of type double, they also work with integers. The compiler automatically promotes an integer values to type double when you use it in an expression such as 12*(len1+len2). We could expand a little further on overloaded operators in the Length class with a working example.
Try It Out
A Value Class with Overloaded Operators
This example implements operator overloading for addition, multiplication, and division for the Length class: // Ex8_09.cpp : main project file. // Overloading operators in the value class, Length #include “stdafx.h” using namespace System; value class Length { private: int feet; int inches; public: static initonly int inchesPerFoot = 12; // Constructor Length(int ft, int ins) : feet(ft), inches(ins){ } // A length as a string virtual String^ ToString() override { return feet+L” feet “ + inches + L” inches”;
}
// Addition operator Length operator+(Length len) { int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet); return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); } // Division operator static Length operator/(Length len, double x) { int ins = safe_cast((len.feet*inchesPerFoot + len.inches)/x); return Length(ins/inchesPerFoot, ins%inchesPerFoot);
464
More on Classes } static Length operator*(double x, Length len); // Pre-multiply by a double value static Length operator*(Length len, double x); // Post-multiply by a double value }; Length Length::operator *(double x, Length len) { int ins = safe_cast(x*len.inches +x*len.feet*inchesPerFoot); return Length(ins/inchesPerFoot, ins%inchesPerFoot); } Length Length::operator *(Length len, double x) { return operator*(x, len); } int main(array<System::String ^> ^args) { Length len1 = Length(6, 9); Length len2 = Length(7, 8); double factor = 2.5; Console::WriteLine(L”{0} Console::WriteLine(L”{0} Console::WriteLine(L”{1} Console::WriteLine(L”The
plus {1} is {2}”, len1, len2, len1+len2); times {1} is {2}”, factor, len2, factor*len2); times {0} is {2}”, factor, len2, len2*factor); sum of {0} and {1} divided by {2} is {3}”, len1, len2, factor, (len1+len2)/factor);
return 0; }
The output from the example is: 6 feet 9 inches plus 7 feet 8 inches is 14 feet 5 inches 2.5 times 7 feet 8 inches is 19 feet 2 inches 7 feet 8 inches times 2.5 is 19 feet 2 inches The sum of 6 feet 9 inches and 7 feet 8 inches divided by 2.5 is 5 feet 9 inches Press any key to continue . . .
How It Works The new operator overloading function in the Length class is for division and it allows division of a Length value by a value of type double. Dividing a double value by a Length object does not have an obvious meaning so there’s no need to implement this version. The operator/() function is implemented as another static member of the class and the definition appears within the body of the class definition to contrast how that looks compared with the operator*() functions. You would normally define all these functions inside the class definition. Of course, you could define the operator/() function as a non-static class member like this: Length operator/(double x) { int ins = safe_cast((feet*inchesPerFoot + inches)/x); return Length(ins/inchesPerFoot, ins%inchesPerFoot); }
465
Chapter 8 It now has one argument that is the right operand for the / operator. The left operand is the current object referenced by the this pointer (implicitly in this case). The operators are exercised in the four output statements. Only the last one is new to you and this combines the use of the overloaded + operator for Length objects with the overloaded / operator. The last argument to the Console::WriteLine() function in the fourth output statement is (len1+len2)/ factor which is equivalent to the expression: Length::operator/(len1.operator+(len2), factor) .ToString()
The first argument to the static operator/() function is the Length object that is returned by the operator+() function, and the second argument is the factor variable, which is the divisor. The ToString() function for the Length object returned by operator/() is called to produce the argument string that is passed to the Console::WriteLine() function. It is possible that you might want the capability to divide one Length object by another and have a value of type int as the result. This would allow you to figure out how many 17 inch lengths you can cut from a piece of timber 12 feet six inches long for instance. You can implement this quite easily like this: static int operator/(Length len1, Length len2) { return (len1.feet*inchesPerFoot + len1.inches)/( len2.feet*inchesPerFoot + len2.inches); }
This just returns the result of dividing the first length in inches by the second in inches. To complete the set you could add a function to overload the % operator to tell you how much is left over. This could be implemented as: static Length operator%(Length len1, Length len2) { int ins = (len1.feet*inchesPerFoot + len1.inches)% (len2.feet*inchesPerFoot + len2.inches); return Length(ins/inchesPerFoot, ins%inchesPerFoot); }
You compute the residue in inches after dividing len1 by len2 and return it as a new Length object. With all these operators you really can use your Length objects in arithmetic expressions. You can write statements such as: Length Length Length Length
len1 = Length(2,6); len2 = Length(3,5); len3 = Length(14,6); total = 12*(len1 + len2 + len3)
// 2 feet 6 inches // 3 feet 5 inches // 14 feet 6 inches + (len3/Length(1,7))*len2;
The value of total will be 275 feet 9 inches. The last statement makes use of the assignment operator that comes with every value class as well as the operator*(), operator+(), and operator/() functions in the Length class. This operator overloading is not only powerful stuff, it really is easy isn’t it?
466
More on Classes
Overloading the Increment and Decrement Operators Overloading the increment and decrement is simpler in C++/CLI that in native C++. As long as you implement the operator function as a static class member, the same function will serve as both the prefix and postfix operator functions. Here’s how you could implement the increment operator for the Length class: class Length { public: // Code as before... // Overloaded increment operator function - increment by 1 inch static Length operator++(Length len) { ++len.inches; len.feet += len.inches/len.inchesPerFoot; len.inches %= len.inchesPerFoot; return len; } }
This implementation of the operator++() function increments a length by 1 inch. The following code exercises the function: Length len = Length(1, 11); Console::WriteLine(len++); Console::WriteLine(++len);
// 1 foot 11 inches
Executing this fragment produces the output: 1 feet 11 inches 2 feet 1 inches
Thus the prefix and postfix increment operations are working as they should using a single operator function in the Length class. This occurs because the compiler is able to determine whether to use the value of the operand in a surrounding expression before or after the operand has been incremented and compile the code accordingly.
Overloading Operators in Reference Classes Overloading operators in a reference class is essentially the same as overloading operators in a value class, the primary difference being that parameters and return values are typically handles. Let’s see how the Length class looks implemented as a reference class; then you can compare the two versions.
Try It Out
Overloaded Operators in a Reference Class
This example defines Length as a reference class with the same set of overloaded operators as the value class version:
467
Chapter 8 // Ex8_10.cpp : main project file. // Defining and using overloaded operator #include “stdafx.h” using namespace System; ref class Length { private: int feet; int inches; public: static initonly int inchesPerFoot = 12; // Constructor Length(int ft, int ins) : feet(ft), inches(ins){ } // A length as a string virtual String^ ToString() override { return feet+L” feet “ + inches + L” inches”;
}
// Overloaded addition operator Length^ operator+(Length^ len) { int inchTotal = inches+len->inches+inchesPerFoot*(feet+len->feet); return gcnew Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); } // Overloaded divide operator - right operand type double static Length^ operator/(Length^ len, double x) { int ins = safe_cast((len->feet*inchesPerFoot + len->inches)/x); return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot); } // Overloaded divide operator - both operands type Length static int operator/(Length^ len1, Length^ len2) { return (len1->feet*inchesPerFoot + len1->inches)/ (len2->feet*inchesPerFoot + len2->inches); } // Overloaded remainder operator static Length^ operator%(Length^ len1, Length^ len2) { int ins = (len1->feet*inchesPerFoot + len1->inches)% (len2->feet*inchesPerFoot + len2->inches); return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot); } static Length^ operator*(double x, Length^ len); // Multiply - R operand double static Length^ operator*(Length^ len, double x); // Multiply - L operand double // Pre- and postfix increment operator